feat(ui): migrate remaining pages to the new shell

Apply the sidebar shell and bold primitives across the rest of the app so
the whole experience shares one visual language:

- Browse/organize: series (keeps stacked-covers/series-card CSS),
  collections (keeps wood-paneling + carousel classes), browse_detail.
- Account/stats: profile + form + modal, progress, analytics (stat-card
  tiles), devices, conflicts (status semantics), queue (status/priority
  badges).
- Admin: admin sidebar restyled with icons, dashboard/users/library/
  processing-issues/settings migrated to .card/.btn/.input primitives.
- Docs/setup/misc: docs (keeps prose/highlight.js), api_explorer, setup
  wizard, unlinked_books, custom_section builder.
- Modals/fragments: collection_modal, collection_rules,
  restore_system_collection_modal, filter_item, book_detail_modals — all
  overlays use --surface-overlay + --shadow-pop.

Every Alpine handler, HTMX attribute, id, name, and data-* is preserved;
only presentation changes.
This commit is contained in:
2026-08-05 16:45:02 -04:00
parent 07578e0206
commit 8ea70ea7f8
62 changed files with 5887 additions and 3267 deletions
+54 -32
View File
@@ -11,60 +11,79 @@ templ Admin(user User) {
</head> </head>
<body x-data="admin" x-init="loadWatchStatus(); initializeScanWebSocket()" class="theme-tokyo-night"> <body x-data="admin" x-init="loadWatchStatus(); initializeScanWebSocket()" class="theme-tokyo-night">
@Header(user, "/admin") @Header(user, "/admin")
<div class="flex min-h-screen" style="background-color: var(--bg-primary)"> <div class="flex">
@AdminSidebar(user, "/admin") @AdminSidebar(user, "/admin")
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="max-w-4xl"> <div class="max-w-4xl">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Dashboard</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("grid", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Dashboard</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="stat-card">
<div class="flex items-center space-x-3"> <div class="flex items-center gap-3">
<div class="text-3xl">📖</div> <span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("library", "h-5 w-5")
</span>
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3> <h3 class="font-semibold" style="color: var(--text-primary)">Library</h3>
<p style="color: var(--text-secondary)" class="text-sm">Manage your ebook collection</p> <p class="text-sm" style="color: var(--text-secondary)">Manage your ebook collection</p>
</div> </div>
</div> </div>
<a href="/" class="mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded">View Library</a> <a href="/" class="btn btn-secondary mt-4 text-sm">
@Icon("arrow-right", "h-4 w-4")
View Library
</a>
</div> </div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="stat-card">
<div class="flex items-center space-x-3"> <div class="flex items-center gap-3">
<div class="text-3xl">👁️</div> <span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("sync", "h-5 w-5")
</span>
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Watch Status</h3> <h3 class="font-semibold" style="color: var(--text-primary)">Scan Watch Status</h3>
<p style="color: var(--text-secondary)" class="text-sm">Auto-detecting new files</p> <p class="text-sm" style="color: var(--text-secondary)">Auto-detecting new files</p>
</div> </div>
</div> </div>
<div id="watch-status" class="mt-4 text-sm" style="color: var(--text-secondary)"> <div id="watch-status" class="mt-4 text-sm" style="color: var(--text-secondary)">
<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span> <span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: var(--status-success);"></span>
Watching <span id="watch-count">0</span> libraries Watching <span id="watch-count">0</span> libraries
</div> </div>
</div> </div>
</div> </div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="card p-6">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3> <h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button @click="scanAllLibraries()" class="btn-primary p-4 rounded-lg text-left"> <button @click="scanAllLibraries()" class="btn btn-primary py-4 flex-col items-start gap-1">
<div class="font-medium">Rescan Library</div> <span class="flex items-center gap-2 font-medium">
<div style="color: var(--text-secondary)" class="text-sm">Re-scan existing files and fix metadata</div> @Icon("refresh", "h-5 w-5")
Rescan Library
</span>
<span class="text-xs font-normal opacity-80">Re-scan existing files and fix metadata</span>
</button> </button>
<a href="/admin/library" class="btn-secondary p-4 rounded-lg text-left block"> <a href="/admin/library" class="btn btn-secondary py-4 flex-col items-start gap-1">
<div class="font-medium">Manage Libraries and Folders</div> <span class="flex items-center gap-2 font-medium">
<div style="color: var(--text-secondary)" class="text-sm">Add or remove libraries and scan directories</div> @Icon("library", "h-5 w-5")
Manage Libraries
</span>
<span class="text-xs font-normal opacity-80">Add or remove libraries and scan directories</span>
</a> </a>
</div> </div>
</div> </div>
<!-- Scan Progress Section --> <!-- Scan Progress Section -->
<div id="scan-progress-container" class="hidden mt-6 p-6 rounded-lg border opacity-0 -translate-y-2.5 transition-all duration-300 ease-out" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div id="scan-progress-container" class="card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold" style="color: var(--text-primary)"> <h3 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
📚 Scanning Libraries @Icon("refresh", "h-5 w-5")
Scanning Libraries
</h3> </h3>
<button @click="hideScanProgress()" class="p-2 hover:bg-gray-700 rounded"> <button @click="hideScanProgress()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
<!-- Overall Progress --> <!-- Overall Progress -->
@@ -73,7 +92,7 @@ templ Admin(user User) {
<span style="color: var(--text-secondary)">Overall Progress</span> <span style="color: var(--text-secondary)">Overall Progress</span>
<span id="scan-progress-text" style="color: var(--text-primary)">0%</span> <span id="scan-progress-text" style="color: var(--text-primary)">0%</span>
</div> </div>
<div class="w-full bg-gray-700 rounded-full h-3"> <div class="w-full rounded-full h-3" style="background-color: var(--surface-hover);">
<div <div
id="scan-progress-bar" id="scan-progress-bar"
class="h-3 rounded-full transition-all duration-500" class="h-3 rounded-full transition-all duration-500"
@@ -89,21 +108,24 @@ templ Admin(user User) {
<!-- Dynamically populated --> <!-- Dynamically populated -->
</div> </div>
<!-- Results Summary --> <!-- Results Summary -->
<div id="scan-results" class="hidden mt-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);"> <div id="scan-results" class="hidden mt-6 p-4 rounded-xl border" style="background-color: var(--bg-primary); border-color: var(--border);">
<h4 class="font-semibold mb-2" style="color: var(--text-primary)"> Scan Complete!</h4> <h4 class="font-semibold mb-2 flex items-center gap-2" style="color: var(--status-success);">
@Icon("check-circle", "h-5 w-5")
Scan Complete!
</h4>
<div id="scan-results-content" style="color: var(--text-secondary)"> <div id="scan-results-content" style="color: var(--text-secondary)">
<!-- Results populated by JS --> <!-- Results populated by JS -->
</div> </div>
<div class="mt-4 flex gap-2"> <div class="mt-4 flex gap-2">
<button <button
@click="window.location.reload()" @click="window.location.reload()"
class="btn-primary px-4 py-2 rounded-lg" class="btn btn-primary"
> >
Refresh to View Books Refresh to View Books
</button> </button>
<button <button
@click="hideScanProgress()" @click="hideScanProgress()"
class="btn-secondary px-4 py-2 rounded-lg" class="btn btn-secondary"
> >
Dismiss Dismiss
</button> </button>
+91 -53
View File
@@ -10,49 +10,69 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</head> </head>
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }"> <body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
@Header(user, "/admin/library") @Header(user, "/admin/library")
<div class="flex min-h-screen" style="background-color: var(--bg-primary)"> <div class="flex">
@AdminSidebar(user, "/admin/library") @AdminSidebar(user, "/admin/library")
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="w-full"> <div class="max-w-5xl">
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between gap-4 flex-wrap mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium"> <a href="/admin" class="btn btn-secondary">
Back to Dashboard @Icon("arrow-left", "h-4 w-4")
Back to Dashboard
</a> </a>
<button data-action="show-create-modal" class="btn-primary px-4 py-2 rounded-lg font-medium"> <button data-action="show-create-modal" class="btn btn-primary">
+ Create Library @Icon("plus", "h-4 w-4")
Create Library
</button> </button>
</div> </div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Library Management</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Manage libraries and configure media scanning</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("library", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Library Management</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Manage libraries and configure media scanning</p>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Libraries Section --> <!-- Libraries Section -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Libraries</h3> <div class="flex items-center gap-2 mb-1">
<p style="color: var(--text-secondary)" class="mb-4">Manage media libraries and their folders</p> @Icon("library", "h-5 w-5 shrink-0")
<div id="libraries-list" class="space-y-3 mb-6"> <h3 class="text-lg font-semibold" style="color: var(--text-primary)">Libraries</h3>
</div>
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage media libraries and their folders</p>
<div id="libraries-list" class="space-y-3 mb-2">
if len(libraries) == 0 { if len(libraries) == 0 {
<p style="color: var(--text-secondary)" class="text-center py-8"> <p class="text-center py-8 text-sm" style="color: var(--text-secondary)">
No libraries yet. Create your first library to get started. No libraries yet. Create your first library to get started.
</p> </p>
} else { } else {
for _, library := range libraries { for _, library := range libraries {
<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="p-4 rounded-xl border transition-colors hover:bg-surface-hover" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start gap-3 mb-2">
<div> <div class="min-w-0">
<h4 class="font-semibold" style="color: var(--text-primary)">{ library.Name }</h4> <h4 class="font-semibold" style="color: var(--text-primary)">{ library.Name }</h4>
if library.Description != "" { if library.Description != "" {
<p class="text-sm" style="color: var(--text-secondary)">{ library.Description }</p> <p class="text-sm" style="color: var(--text-secondary)">{ library.Description }</p>
} }
<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)"> <span class="chip mt-1">
@Icon("tag", "h-3 w-3")
{ library.TypeName } { library.TypeName }
</span> </span>
</div> </div>
<div class="flex space-x-2"> <div class="flex flex-wrap gap-1 shrink-0">
<button data-library-id={ library.ID } data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button> <button data-library-id={ library.ID } data-action="show-folders" class="btn btn-secondary text-xs px-2.5 py-1">
<button data-library-id={ library.ID } data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button> @Icon("folder", "h-4 w-4")
<button data-library-id={ library.ID } data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button> Folders
</button>
<button data-library-id={ library.ID } data-action="edit" class="btn btn-primary text-xs px-2.5 py-1">
@Icon("edit", "h-4 w-4")
Edit
</button>
<button data-library-id={ library.ID } data-action="delete" class="btn btn-danger text-xs px-2.5 py-1">
@Icon("trash", "h-4 w-4")
Delete
</button>
</div> </div>
</div> </div>
<div id={ "library-folders-" + library.ID } class="hidden mt-3 space-y-2"></div> <div id={ "library-folders-" + library.ID } class="hidden mt-3 space-y-2"></div>
@@ -62,20 +82,26 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</div> </div>
</div> </div>
<!-- User Library Visibility Section --> <!-- User Library Visibility Section -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Library Visibility</h3> <div class="flex items-center gap-2 mb-1">
<p style="color: var(--text-secondary)" class="mb-4">Control which libraries are visible to users</p> @Icon("check-circle", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Library Visibility</h3>
</div>
<p class="text-sm mb-4" style="color: var(--text-secondary)">Control which libraries are visible to users</p>
<div id="visibility-controls" class="space-y-4"> <div id="visibility-controls" class="space-y-4">
<!-- Visibility controls will be loaded here --> <!-- Visibility controls will be loaded here -->
</div> </div>
</div> </div>
</div> </div>
<!-- User Visibility Management --> <!-- User Visibility Management -->
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="mt-6 card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">User Library Access</h3> <div class="flex items-center gap-2 mb-1">
<p style="color: var(--text-secondary)" class="mb-4">Manage individual user access to specific libraries</p> @Icon("users", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">User Library Access</h3>
</div>
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage individual user access to specific libraries</p>
<div class="mb-4"> <div class="mb-4">
<select id="user-select" onchange="loadUserVisibility()" class="px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"> <select id="user-select" onchange="loadUserVisibility()" class="input w-auto">
<option value="">Select a user...</option> <option value="">Select a user...</option>
for _, user := range users { for _, user := range users {
<option value="{ user.ID }">{ user.Username } ({ user.Email })</option> <option value="{ user.ID }">{ user.Username } ({ user.Email })</option>
@@ -90,43 +116,50 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</main> </main>
</div> </div>
<!-- Create Library Modal --> <!-- Create Library Modal -->
<div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
<button type="button" data-action="hide-create-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" data-action="hide-create-modal" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<form id="create-library-form"> <form id="create-library-form">
<input type="hidden" id="library-id" name="id"/> <input type="hidden" id="library-id" name="id"/>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
<input type="text" name="name" placeholder="My Ebook Library" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required/> <input type="text" name="name" placeholder="My Ebook Library" class="input" required/>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Description</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
<textarea name="description" placeholder="Optional description" rows="3" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"></textarea> <textarea name="description" placeholder="Optional description" rows="3" class="input"></textarea>
</div> </div>
<div class="mb-4"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Type</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Type</label>
<select name="type" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required> <select name="type" class="input" required>
<option value="ebooks">📚 Ebooks</option> <option value="ebooks">Ebooks</option>
<option value="comics">📖 Comics</option> <option value="comics">Comics</option>
<option value="manga">🗾 Manga</option> <option value="manga">Manga</option>
</select> </select>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" data-action="hide-create-modal" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" data-action="hide-create-modal" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Create</button> <button type="submit" class="btn btn-primary">
@Icon("plus", "h-4 w-4")
Create
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Folder Browser Modal --> <!-- Folder Browser Modal -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <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 rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" data-action="browse-cancel" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" data-action="browse-cancel" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<div id="folder-browser-content"> <div id="folder-browser-content">
<!-- Directory listings will be rendered here --> <!-- Directory listings will be rendered here -->
@@ -134,18 +167,23 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</div> </div>
</div> </div>
<!-- Delete Library Confirmation Modal --> <!-- Delete Library Confirmation Modal -->
<div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
<button type="button" data-action="hide-delete-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" data-action="hide-delete-modal" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<div id="delete-modal-content" class="mb-6" style="color: var(--text-primary)"> <div id="delete-modal-content" class="mb-6" style="color: var(--text-primary)">
<!-- Dynamic content will be injected here --> <!-- Dynamic content will be injected here -->
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" data-action="hide-delete-modal" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" data-action="hide-delete-modal" class="btn btn-secondary">Cancel</button>
<button type="button" data-action="confirm-delete" class="btn-primary px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600">Delete</button> <button type="button" data-action="confirm-delete" class="btn btn-danger">
@Icon("trash", "h-4 w-4")
Delete
</button>
</div> </div>
</div> </div>
</div> </div>
+143 -27
View File
@@ -37,7 +37,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,161 +45,277 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"w-full\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><a href=\"/admin\" class=\"btn-secondary px-4 py-2 rounded-lg font-medium\">← Back to Dashboard</a> <button data-action=\"show-create-modal\" class=\"btn-primary px-4 py-2 rounded-lg font-medium\">+ Create Library</button></div><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Library Management</h1><p style=\"color: var(--text-secondary)\">Manage libraries and configure media scanning</p></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-8\"><!-- Libraries Section --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Libraries</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Manage media libraries and their folders</p><div id=\"libraries-list\" class=\"space-y-3 mb-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><a href=\"/admin\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Dashboard</a> <button data-action=\"show-create-modal\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Create Library</button></div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Library Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage libraries and configure media scanning</p></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6\"><!-- Libraries Section --><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Libraries</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Manage media libraries and their folders</p><div id=\"libraries-list\" class=\"space-y-3 mb-2\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(libraries) == 0 { if len(libraries) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p style=\"color: var(--text-secondary)\" class=\"text-center py-8\">No libraries yet. Create your first library to get started.</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"text-center py-8 text-sm\" style=\"color: var(--text-secondary)\">No libraries yet. Create your first library to get started.</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
for _, library := range libraries { for _, library := range libraries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"p-4 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border)\"><div class=\"flex justify-between items-start mb-2\"><div><h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"p-4 rounded-xl border transition-colors hover:bg-surface-hover\" style=\"background-color: var(--bg-primary); border-color: var(--border)\"><div class=\"flex justify-between items-start gap-3 mb-2\"><div class=\"min-w-0\"><h4 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 44, Col: 89} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 54, Col: 89}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h4>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h4>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if library.Description != "" { if library.Description != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 46, Col: 92} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 56, Col: 92}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"inline-block px-2 py-1 text-xs rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"chip mt-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("tag", "h-3 w-3").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 49, Col: 33} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 60, Col: 33}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></div><div class=\"flex space-x-2\"><button data-library-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"flex flex-wrap gap-1 shrink-0\"><button data-library-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 53, Col: 50} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 64, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" data-action=\"show-folders\" class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--bg-secondary); color: var(--text-primary)\">Folders</button> <button data-library-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-action=\"show-folders\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("folder", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Folders</button> <button data-library-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 54, Col: 50} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 68, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-action=\"edit\" class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">Edit</button> <button data-library-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-action=\"edit\" class=\"btn btn-primary text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Edit</button> <button data-library-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 55, Col: 50} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 72, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" data-action=\"delete\" class=\"text-xs px-2 py-1 rounded text-red-500\">Delete</button></div></div><div id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-action=\"delete\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Delete</button></div></div><div id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-folders-" + library.ID) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-folders-" + library.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 58, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 78, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"hidden mt-3 space-y-2\"></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"hidden mt-3 space-y-2\"></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div><!-- User Library Visibility Section --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Library Visibility</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Control which libraries are visible to users</p><div id=\"visibility-controls\" class=\"space-y-4\"><!-- Visibility controls will be loaded here --></div></div></div><!-- User Visibility Management --><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">User Library Access</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Manage individual user access to specific libraries</p><div class=\"mb-4\"><select id=\"user-select\" onchange=\"loadUserVisibility()\" class=\"px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"><option value=\"\">Select a user...</option> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div><!-- User Library Visibility Section --><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Library Visibility</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Control which libraries are visible to users</p><div id=\"visibility-controls\" class=\"space-y-4\"><!-- Visibility controls will be loaded here --></div></div></div><!-- User Visibility Management --><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("users", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">User Library Access</h3></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Manage individual user access to specific libraries</p><div class=\"mb-4\"><select id=\"user-select\" onchange=\"loadUserVisibility()\" class=\"input w-auto\"><option value=\"\">Select a user...</option> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, user := range users { for _, user := range users {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<option value=\"{ user.ID }\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<option value=\"{ user.ID }\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 81, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 107, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " (") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " (")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 81, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 107, Col: 69}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ")</option>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ")</option>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</select></div><div id=\"user-libraries\" class=\"space-y-3\"><!-- User library checkboxes will be loaded here --></div></div></div></main></div><!-- Create Library Modal --><div id=\"create-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" data-action=\"hide-create-modal\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"create-library-form\"><input type=\"hidden\" id=\"library-id\" name=\"id\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Library Type</label> <select name=\"type\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required><option value=\"ebooks\">📚 Ebooks</option> <option value=\"comics\">📖 Comics</option> <option value=\"manga\">🗾 Manga</option></select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-create-modal\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Create</button></div></form></div></div><!-- Folder Browser Modal --><div id=\"folder-browser-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><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\" data-action=\"browse-cancel\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div id=\"folder-browser-content\"><!-- Directory listings will be rendered here --></div></div></div><!-- Delete Library Confirmation Modal --><div id=\"delete-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" data-action=\"hide-delete-modal\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div id=\"delete-modal-content\" class=\"mb-6\" style=\"color: var(--text-primary)\"><!-- Dynamic content will be injected here --></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-delete-modal\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" data-action=\"confirm-delete\" class=\"btn-primary px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600\">Delete</button></div></div></div><script src=\"/static/htmx.min.js\"></script></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</select></div><div id=\"user-libraries\" class=\"space-y-3\"><!-- User library checkboxes will be loaded here --></div></div></div></main></div><!-- Create Library Modal --><div id=\"create-library-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\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" data-action=\"hide-create-modal\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</button></div><form id=\"create-library-form\"><input type=\"hidden\" id=\"library-id\" name=\"id\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"input\" required></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"input\"></textarea></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Type</label> <select name=\"type\" class=\"input\" required><option value=\"ebooks\">Ebooks</option> <option value=\"comics\">Comics</option> <option value=\"manga\">Manga</option></select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-create-modal\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Create</button></div></form></div></div><!-- Folder Browser Modal --><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\" style=\"box-shadow: var(--shadow-pop);\"><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\" data-action=\"browse-cancel\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</button></div><div id=\"folder-browser-content\"><!-- Directory listings will be rendered here --></div></div></div><!-- Delete Library Confirmation Modal --><div id=\"delete-library-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\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" data-action=\"hide-delete-modal\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</button></div><div id=\"delete-modal-content\" class=\"mb-6\" style=\"color: var(--text-primary)\"><!-- Dynamic content will be injected here --></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" data-action=\"hide-delete-modal\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" data-action=\"confirm-delete\" class=\"btn btn-danger\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "Delete</button></div></div></div><script src=\"/static/htmx.min.js\"></script></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+93 -72
View File
@@ -1,4 +1,3 @@
package templates package templates
templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssueData, stats IssueStats) { templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssueData, stats IssueStats) {
@@ -12,83 +11,105 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
<body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }"> <body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }">
@Header(user, "/admin/libraries/"+libraryID) @Header(user, "/admin/libraries/"+libraryID)
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="mb-8"> <div class="mx-auto max-w-4xl">
<div class="flex items-center justify-between mb-4"> <div class="mb-8">
<div> <div class="flex items-center justify-between gap-4 flex-wrap mb-4">
<h1 class="text-3xl font-bold mb-2">Processing Issues</h1> <div>
<p class="text-gray-600">Items that couldn't be processed in this library</p> <div class="flex items-center gap-3 mb-1">
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("alert", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Processing Issues</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Items that couldn't be processed in this library</p>
</div>
<a href="/admin/libraries/{ libraryID }" class="btn btn-secondary">
@Icon("arrow-left", "h-4 w-4")
Back to Library
</a>
</div> </div>
<a href="/admin/libraries/{ libraryID }" class="btn-secondary px-4 py-2 rounded-lg">
Back to Library
</a>
</div> </div>
</div> if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 { <!-- Stats Cards -->
<!-- Stats Cards --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> if stats.ErrorCount > 0 {
if stats.ErrorCount > 0 { <div class="stat-card" style="border-left: 4px solid var(--status-danger);">
<div class="card p-6 rounded-lg border-l-4 border-red-500"> <div class="flex items-center gap-2 mb-2" style="color: var(--status-danger);">
<h3 class="text-lg font-semibold text-red-600 mb-2">Errors</h3> @Icon("x-circle", "h-5 w-5")
<p class="text-3xl font-bold">{ stats.ErrorCount }</p> <h3 class="text-sm font-semibold uppercase tracking-wide">Errors</h3>
</div> </div>
} <p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.ErrorCount }</p>
if stats.WarningCount > 0 { </div>
<div class="card p-6 rounded-lg border-l-4 border-yellow-500"> }
<h3 class="text-lg font-semibold text-yellow-600 mb-2">Warnings</h3> if stats.WarningCount > 0 {
<p class="text-3xl font-bold">{ stats.WarningCount }</p> <div class="stat-card" style="border-left: 4px solid var(--status-warning);">
</div> <div class="flex items-center gap-2 mb-2" style="color: var(--status-warning);">
} @Icon("alert", "h-5 w-5")
if stats.InfoCount > 0 { <h3 class="text-sm font-semibold uppercase tracking-wide">Warnings</h3>
<div class="card p-6 rounded-lg border-l-4 border-blue-500"> </div>
<h3 class="text-lg font-semibold text-blue-600 mb-2">Info</h3> <p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.WarningCount }</p>
<p class="text-3xl font-bold">{ stats.InfoCount }</p> </div>
</div> }
} if stats.InfoCount > 0 {
</div> <div class="stat-card" style="border-left: 4px solid var(--status-info);">
} <div class="flex items-center gap-2 mb-2" style="color: var(--status-info);">
if len(issues) == 0 { @Icon("info", "h-5 w-5")
<div class="card p-8 rounded-lg text-center"> <h3 class="text-sm font-semibold uppercase tracking-wide">Info</h3>
<p class="text-gray-600">No processing issues found for this library.</p> </div>
</div> <p class="text-3xl font-bold" style="color: var(--text-primary)">{ stats.InfoCount }</p>
} else { </div>
<!-- Issues List --> }
<div class="space-y-4"> </div>
for _, issue := range issues { }
<div class="card p-6 rounded-lg"> if len(issues) == 0 {
<div class="flex justify-between items-start mb-4"> <div class="card p-8 text-center">
<div class="flex-1"> <span class="grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
<h4 class="text-lg font-semibold mb-2">{ issue.Title }</h4> @Icon("check-circle", "h-6 w-6")
<p class="text-gray-700 mb-3">{ issue.IssueDescription }</p> </span>
<div class="text-sm text-gray-500 space-y-1"> <p class="text-sm" style="color: var(--text-secondary)">No processing issues found for this library.</p>
<p><strong>Type:</strong> { issue.IssueType }</p> </div>
<p><strong>Format:</strong> { issue.FormatGroup }</p> } else {
<p><strong>File:</strong> { issue.FilePath }</p> <!-- Issues List -->
<p><strong>Library:</strong> { issue.LibraryTypeName }</p> <div class="space-y-4">
for _, issue := range issues {
<div class="card p-6">
<div class="flex justify-between items-start gap-4 mb-4">
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ issue.Title }</h4>
<p class="mb-3 text-sm" style="color: var(--text-secondary)">{ issue.IssueDescription }</p>
<div class="text-sm space-y-1" style="color: var(--text-secondary)">
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Type:</span> { issue.IssueType }</p>
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Format:</span> { issue.FormatGroup }</p>
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">File:</span> { issue.FilePath }</p>
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Library:</span> { issue.LibraryTypeName }</p>
</div>
</div>
<div class="ml-2 shrink-0">
if issue.Severity == "error" {
<span class="badge status-failed">{ issue.Severity }</span>
} else if issue.Severity == "warning" {
<span class="badge status-pending">{ issue.Severity }</span>
} else {
<span class="badge status-processing">{ issue.Severity }</span>
}
</div> </div>
</div> </div>
<div class="ml-4"> <div class="flex gap-3 mt-4">
<span if issue.Severity == "warning" || issue.Severity == "info" {
class="inline-block px-3 py-1 text-sm rounded-full font-medium" <button
style="background-color: var(--accent); color: white;" @click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
> class="btn btn-secondary text-sm"
{ issue.Severity } >
</span> @Icon("close", "h-4 w-4")
Dismiss
</button>
}
</div> </div>
</div> </div>
<div class="flex gap-3 mt-4"> }
if issue.Severity == "warning" || issue.Severity == "info" { </div>
<button }
@click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')" </div>
class="btn-secondary px-4 py-2 rounded text-sm"
>
Dismiss
</button>
}
</div>
</div>
}
</div>
}
</main> </main>
</body> </body>
</html> </html>
+147 -46
View File
@@ -1,7 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020 // templ: version: v0.3.1020
package templates package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -38,200 +37,302 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"flex-1 p-8\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><div><h1 class=\"text-3xl font-bold mb-2\">Processing Issues</h1><p class=\"text-gray-600\">Items that couldn't be processed in this library</p></div><a href=\"/admin/libraries/{ libraryID }\" class=\"btn-secondary px-4 py-2 rounded-lg\">← Back to Library</a></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"flex-1 p-8\"><div class=\"mx-auto max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Processing Issues</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Items that couldn't be processed in this library</p></div><a href=\"/admin/libraries/{ libraryID }\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Library</a></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 { if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if stats.ErrorCount > 0 { if stats.ErrorCount > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-6 rounded-lg border-l-4 border-red-500\"><h3 class=\"text-lg font-semibold text-red-600 mb-2\">Errors</h3><p class=\"text-3xl font-bold\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-danger);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-danger);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("x-circle", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Errors</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 32, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 41, Col: 92}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if stats.WarningCount > 0 { if stats.WarningCount > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border-l-4 border-yellow-500\"><h3 class=\"text-lg font-semibold text-yellow-600 mb-2\">Warnings</h3><p class=\"text-3xl font-bold\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-warning);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-warning);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Warnings</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 38, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 50, Col: 94}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if stats.InfoCount > 0 { if stats.InfoCount > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"card p-6 rounded-lg border-l-4 border-blue-500\"><h3 class=\"text-lg font-semibold text-blue-600 mb-2\">Info</h3><p class=\"text-3xl font-bold\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-info);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-info);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("info", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Info</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 44, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 59, Col: 91}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if len(issues) == 0 { if len(issues) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card p-8 rounded-lg text-center\"><p class=\"text-gray-600\">No processing issues found for this library.</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-8 text-center\"><span class=\"grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No processing issues found for this library.</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Issues List --> <div class=\"space-y-4\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<!-- Issues List --> <div class=\"space-y-4\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, issue := range issues { for _, issue := range issues {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card p-6 rounded-lg\"><div class=\"flex justify-between items-start mb-4\"><div class=\"flex-1\"><h4 class=\"text-lg font-semibold mb-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"card p-6\"><div class=\"flex justify-between items-start gap-4 mb-4\"><div class=\"flex-1 min-w-0\"><h4 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 60, Col: 62} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 98}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h4><p class=\"text-gray-700 mb-3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h4><p class=\"mb-3 text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 61, Col: 64} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 96}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p><div class=\"text-sm text-gray-500 space-y-1\"><p><strong>Type:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</p><div class=\"text-sm space-y-1\" style=\"color: var(--text-secondary)\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Type:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 63, Col: 54} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 81, Col: 140}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p><p><strong>Format:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Format:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 64, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 82, Col: 144}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</p><p><strong>File:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">File:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 65, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 83, Col: 139}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p><p><strong>Library:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Library:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 66, Col: 63} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 149}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p></div></div><div class=\"ml-4\"><span class=\"inline-block px-3 py-1 text-sm rounded-full font-medium\" style=\"background-color: var(--accent); color: white;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p></div></div><div class=\"ml-2 shrink-0\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string if issue.Severity == "error" {
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"badge status-failed\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 27} return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) var templ_7745c5c3_Var11 string
if templ_7745c5c3_Err != nil { templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
return templ_7745c5c3_Err if templ_7745c5c3_Err != nil {
} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 89, Col: 62}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div></div><div class=\"flex gap-3 mt-4\">") }
if templ_7745c5c3_Err != nil { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
return templ_7745c5c3_Err if templ_7745c5c3_Err != nil {
} return templ_7745c5c3_Err
if issue.Severity == "warning" || issue.Severity == "info" { }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn-secondary px-4 py-2 rounded text-sm\">Dismiss</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if issue.Severity == "warning" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"badge status-pending\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 91, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span class=\"badge status-processing\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 93, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></div><div class=\"flex gap-3 mt-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if issue.Severity == "warning" || issue.Severity == "info" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Dismiss</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</main></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></main></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+41 -26
View File
@@ -11,55 +11,67 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
</head> </head>
<body class="theme-{ user.Theme }" x-data="adminSettings"> <body class="theme-{ user.Theme }" x-data="adminSettings">
@Header(user, "/admin/settings") @Header(user, "/admin/settings")
<div class="flex min-h-screen" style="background-color: var(--bg-primary)"> <div class="flex">
@AdminSidebar(user, "/admin/settings") @AdminSidebar(user, "/admin/settings")
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="max-w-4xl"> <div class="max-w-4xl">
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between gap-4 flex-wrap mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium"> <a href="/admin" class="btn btn-secondary">
Back to Dashboard @Icon("arrow-left", "h-4 w-4")
Back to Dashboard
</a> </a>
</div> </div>
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">System Settings</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Configure your Bookhoard instance</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("settings", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">System Settings</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
</div> </div>
if errorMessage != "" { if errorMessage != "" {
<div class="mb-6 p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--error); color: var(--error);"> <div class="mb-6 p-4 rounded-xl border flex items-start gap-3" style="background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);">
{ errorMessage } @Icon("alert", "h-5 w-5 shrink-0 mt-0.5")
<span>{ errorMessage }</span>
</div> </div>
} }
<form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML"> <form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Base URL</h3> <div class="flex items-center gap-2 mb-6">
@Icon("globe", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Base URL</h3>
</div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Base URL</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Base URL</label>
<input <input
type="url" type="url"
name="base_url" name="base_url"
value={ systemConfig["base_url"] } value={ systemConfig["base_url"] }
placeholder="https://books.example.com" placeholder="https://books.example.com"
class="w-full px-4 py-2 rounded-lg border" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
required required
/> />
<p class="text-sm mt-1" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p> <p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p>
</div> </div>
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium"> <button type="submit" class="btn btn-primary">
@Icon("save", "h-4 w-4")
Save Settings Save Settings
</button> </button>
</div> </div>
</div> </div>
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="mt-6 card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">System Defaults</h3> <div class="flex items-center gap-2 mb-6">
@Icon("clock", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">System Defaults</h3>
</div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Default Timezone</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Default Timezone</label>
<select <select
name="default_timezone" name="default_timezone"
id="default_timezone" id="default_timezone"
class="w-full px-4 py-2 rounded-lg border" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="UTC" selected?={ systemConfig["default_timezone"] == "UTC" }>UTC (UTC+0)</option> <option value="UTC" selected?={ systemConfig["default_timezone"] == "UTC" }>UTC (UTC+0)</option>
<option value="Pacific/Honolulu" selected?={ systemConfig["default_timezone"] == "Pacific/Honolulu" }>Hawaii (UTC-10)</option> <option value="Pacific/Honolulu" selected?={ systemConfig["default_timezone"] == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
@@ -86,16 +98,19 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
<option value="Australia/Sydney" selected?={ systemConfig["default_timezone"] == "Australia/Sydney" }>Australian Eastern (UTC+10/+11)</option> <option value="Australia/Sydney" selected?={ systemConfig["default_timezone"] == "Australia/Sydney" }>Australian Eastern (UTC+10/+11)</option>
<option value="Pacific/Auckland" selected?={ systemConfig["default_timezone"] == "Pacific/Auckland" }>New Zealand (UTC+12/+13)</option> <option value="Pacific/Auckland" selected?={ systemConfig["default_timezone"] == "Pacific/Auckland" }>New Zealand (UTC+12/+13)</option>
</select> </select>
<p class="text-sm mt-1" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p> <p class="text-sm mt-2" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
</div> </div>
</div> </div>
</form> </form>
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="mt-6 card p-6">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">URL Paths</h3> <div class="flex items-center gap-2 mb-4">
@Icon("external", "h-5 w-5 shrink-0")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">URL Paths</h3>
</div>
<div class="space-y-2 text-sm" style="color: var(--text-secondary);"> <div class="space-y-2 text-sm" style="color: var(--text-secondary);">
<p><strong>OPDS:</strong> { systemConfig["base_url"] }/opds</p> <p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">OPDS:</span> { systemConfig["base_url"] }/opds</p>
<p><strong>API:</strong> { systemConfig["base_url"] }/api</p> <p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">API:</span> { systemConfig["base_url"] }/api</p>
<p><strong>Device Sync:</strong> { systemConfig["base_url"] }/api/sync</p> <p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Device Sync:</span> { systemConfig["base_url"] }/api/sync</p>
</div> </div>
</div> </div>
</div> </div>
+141 -85
View File
@@ -37,7 +37,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,322 +45,378 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><a href=\"/admin\" class=\"btn-secondary px-4 py-2 rounded-lg font-medium\">← Back to Dashboard</a></div><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">System Settings</h1><p style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><a href=\"/admin\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Dashboard</a></div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("settings", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">System Settings</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if errorMessage != "" { if errorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"mb-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--error); color: var(--error);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-6 p-4 rounded-xl border flex items-start gap-3\" style=\"background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("alert", "h-5 w-5 shrink-0 mt-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 29, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 36, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">Base URL</h3><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("globe", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Base URL</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"]) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 40, Col: 42} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 50, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"https://books.example.com\" class=\"w-full px-4 py-2 rounded-lg border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" required><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn-primary px-6 py-2 rounded-lg font-medium\">Save Settings</button></div></div><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-6\" style=\"color: var(--text-primary)\">System Defaults</h3><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-primary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"w-full px-4 py-2 rounded-lg border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"UTC\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" placeholder=\"https://books.example.com\" class=\"input\" required><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Save Settings</button></div></div><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("clock", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">System Defaults</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"input\"><option value=\"UTC\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "UTC" { if systemConfig["default_timezone"] == "UTC" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "Pacific/Honolulu" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "America/Anchorage" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "America/Los_Angeles" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "America/Denver" { if systemConfig["default_timezone"] == "Pacific/Honolulu" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "America/Phoenix" { if systemConfig["default_timezone"] == "America/Anchorage" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "America/Chicago" { if systemConfig["default_timezone"] == "America/Los_Angeles" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "America/New_York" { if systemConfig["default_timezone"] == "America/Denver" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "America/Sao_Paulo" { if systemConfig["default_timezone"] == "America/Phoenix" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Europe/London" { if systemConfig["default_timezone"] == "America/Chicago" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Europe/Paris" { if systemConfig["default_timezone"] == "America/New_York" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Europe/Helsinki" { if systemConfig["default_timezone"] == "America/Sao_Paulo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Europe/Moscow" { if systemConfig["default_timezone"] == "Europe/London" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Tehran" { if systemConfig["default_timezone"] == "Europe/Paris" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Dubai" { if systemConfig["default_timezone"] == "Europe/Helsinki" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Karachi" { if systemConfig["default_timezone"] == "Europe/Moscow" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Kolkata" { if systemConfig["default_timezone"] == "Asia/Tehran" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Dhaka" { if systemConfig["default_timezone"] == "Asia/Dubai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Bangkok" { if systemConfig["default_timezone"] == "Asia/Karachi" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Shanghai" { if systemConfig["default_timezone"] == "Asia/Kolkata" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Asia/Tokyo" { if systemConfig["default_timezone"] == "Asia/Dhaka" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Australia/Darwin" { if systemConfig["default_timezone"] == "Asia/Bangkok" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Australia/Sydney" { if systemConfig["default_timezone"] == "Asia/Shanghai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if systemConfig["default_timezone"] == "Pacific/Auckland" { if systemConfig["default_timezone"] == "Asia/Tokyo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-8 card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">URL Paths</h3><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><strong>OPDS:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "Australia/Darwin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "Australia/Sydney" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if systemConfig["default_timezone"] == "Pacific/Auckland" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("external", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">URL Paths</h3></div><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">OPDS:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"]) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 96, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 111, Col: 145}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "/opds</p><p><strong>API:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/opds</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">API:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"]) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 97, Col: 59} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 112, Col: 144}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "/api</p><p><strong>Device Sync:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "/api</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Device Sync:</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"]) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 98, Col: 67} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 113, Col: 152}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "/api/sync</p></div></div></div></main></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "/api/sync</p></div></div></div></main></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+21 -12
View File
@@ -1,21 +1,30 @@
package templates package templates
templ AdminSidebar(user User, currentPath string) { templ AdminSidebar(user User, currentPath string) {
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)"> <aside class="w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="p-6"> <div class="p-5">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2> <div class="flex items-center gap-2 mb-6 px-2">
<nav class="space-y-2"> <span class="grid place-items-center h-8 w-8 rounded-lg shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
<a href="/admin" class={activeClass(currentPath, "/admin")} style="color: var(--text-primary)"> @Icon("shield", "h-4 w-4")
🏠 Dashboard </span>
<h2 class="text-xs font-bold uppercase tracking-wide" style="color: var(--text-primary)">Admin Panel</h2>
</div>
<nav class="space-y-0.5">
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
@Icon("grid", "h-5 w-5 shrink-0")
<span>Dashboard</span>
</a> </a>
<a href="/admin/users" class={activeClass(currentPath, "/admin/users")} style="color: var(--text-primary)"> <a href="/admin/users" class={ activeClass(currentPath, "/admin/users") }>
👤 User Administration @Icon("users", "h-5 w-5 shrink-0")
<span>Users</span>
</a> </a>
<a href="/admin/library" class={activeClass(currentPath, "/admin/library")} style="color: var(--text-primary)"> <a href="/admin/library" class={ activeClass(currentPath, "/admin/library") }>
📚 Library Management @Icon("library", "h-5 w-5 shrink-0")
<span>Library</span>
</a> </a>
<a href="/admin/settings" class={activeClass(currentPath, "/admin/settings")} style="color: var(--text-primary)"> <a href="/admin/settings" class={ activeClass(currentPath, "/admin/settings") }>
⚙️ System Settings @Icon("settings", "h-5 w-5 shrink-0")
<span>Settings</span>
</a> </a>
</nav> </nav>
</div> </div>
+49 -9
View File
@@ -29,7 +29,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"w-64 border-r\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"p-6\"><h2 class=\"text-lg font-semibold mb-6\" style=\"color: var(--text-primary)\">Admin Panel</h2><nav class=\"space-y-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"p-5\"><div class=\"flex items-center gap-2 mb-6 px-2\"><span class=\"grid place-items-center h-8 w-8 rounded-lg shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("shield", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><h2 class=\"text-xs font-bold uppercase tracking-wide\" style=\"color: var(--text-primary)\">Admin Panel</h2></div><nav class=\"space-y-0.5\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -38,7 +46,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/admin\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/admin\" class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -51,7 +59,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" style=\"color: var(--text-primary)\">🏠 Dashboard</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("grid", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>Dashboard</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -60,7 +76,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a href=\"/admin/users\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/admin/users\" class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -73,7 +89,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"color: var(--text-primary)\">👤 User Administration</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("users", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Users</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -82,7 +106,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/admin/library\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"/admin/library\" class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -95,7 +119,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" style=\"color: var(--text-primary)\">📚 Library Management</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Library</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -104,7 +136,7 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"/admin/settings\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"/admin/settings\" class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -117,7 +149,15 @@ func AdminSidebar(user User, currentPath string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" style=\"color: var(--text-primary)\">⚙️ System Settings</a></nav></div></aside>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("settings", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span>Settings</span></a></nav></div></aside>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+74 -2
View File
@@ -37,7 +37,7 @@ func Admin(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,7 +45,79 @@ func Admin(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Dashboard</h1><p style=\"color: var(--text-secondary)\">Overview of your Bookhoard library and settings</p></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8\"><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"flex items-center space-x-3\"><div class=\"text-3xl\">📖</div><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Library</h3><p style=\"color: var(--text-secondary)\" class=\"text-sm\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"mt-4 inline-block text-sm btn-secondary px-3 py-1 rounded\">View Library</a></div><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><div class=\"flex items-center space-x-3\"><div class=\"text-3xl\">👁️</div><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Scan Watch Status</h3><p style=\"color: var(--text-secondary)\" class=\"text-sm\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full bg-green-500 mr-2\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn-primary p-4 rounded-lg text-left\"><div class=\"font-medium\">Rescan Library</div><div style=\"color: var(--text-secondary)\" class=\"text-sm\">Re-scan existing files and fix metadata</div></button> <a href=\"/admin/library\" class=\"btn-secondary p-4 rounded-lg text-left block\"><div class=\"font-medium\">Manage Libraries and Folders</div><div style=\"color: var(--text-secondary)\" class=\"text-sm\">Add or remove libraries and scan directories</div></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"hidden mt-6 p-6 rounded-lg border opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">📚 Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"p-2 hover:bg-gray-700 rounded\">✕</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full bg-gray-700 rounded-full h-3\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><!-- Per-Library Progress --><div id=\"library-progress-list\" class=\"space-y-3\"><!-- Dynamically populated --></div><!-- Results Summary --><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">✅ Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"><!-- Results populated by JS --></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn-primary px-4 py-2 rounded-lg\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Dismiss</button></div></div></div></div></main></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("grid", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Dashboard</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Overview of your Bookhoard library and settings</p></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6\"><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Library</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"btn btn-secondary mt-4 text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "View Library</a></div><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Scan Watch Status</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full mr-2\" style=\"background-color: var(--status-success);\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn btn-primary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Rescan Library</span> <span class=\"text-xs font-normal opacity-80\">Re-scan existing files and fix metadata</span></button> <a href=\"/admin/library\" class=\"btn btn-secondary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Manage Libraries</span> <span class=\"text-xs font-normal opacity-80\">Add or remove libraries and scan directories</span></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full rounded-full h-3\" style=\"background-color: var(--surface-hover);\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><!-- Per-Library Progress --><div id=\"library-progress-list\" class=\"space-y-3\"><!-- Dynamically populated --></div><!-- Results Summary --><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-xl border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2 flex items-center gap-2\" style=\"color: var(--status-success);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"><!-- Results populated by JS --></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn btn-primary\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn btn-secondary\">Dismiss</button></div></div></div></div></main></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+119 -109
View File
@@ -13,119 +13,129 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
@Header(currentUser, "/admin/users") @Header(currentUser, "/admin/users")
<!-- Modal Container (populated by HTMX) --> <!-- Modal Container (populated by HTMX) -->
<div id="modal-container"></div> <div id="modal-container"></div>
<div class="flex min-h-screen" style="background-color: var(--bg-primary)"> <div class="flex">
@AdminSidebar(currentUser, "/admin/users") @AdminSidebar(currentUser, "/admin/users")
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="w-full"> <div class="max-w-5xl">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1> <div class="mb-8">
<p style="color: var(--text-secondary)">Manage user accounts and permissions</p> <div class="flex items-center gap-3 mb-1">
</div> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
<!-- Users Table --> @Icon("users", "h-5 w-5")
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)"> </span>
<table class="w-full"> <h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">User Management</h1>
<thead style="background-color: var(--bg-primary)"> </div>
<tr> <p class="text-sm" style="color: var(--text-secondary)">Manage user accounts and permissions</p>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Username</th> </div>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Email</th> <!-- Users Table -->
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Role</th> <div class="card overflow-hidden">
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Created</th> <table class="w-full">
<th class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Actions</th> <thead style="background-color: var(--bg-primary)">
</tr> <tr>
</thead> <th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Username</th>
<tbody class="divide-y" style="divide-color: var(--border)"> <th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Email</th>
for _, user := range users { <th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Role</th>
<tr id={ "user-" + user.ID } class="hover:bg-opacity-50" style="transition: background-color 0.2s;"> <th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Created</th>
<!-- Username --> <th class="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Actions</th>
<td class="px-6 py-4 whitespace-nowrap"> </tr>
<div class="flex items-center"> </thead>
<div> <tbody class="divide-y" style="divide-color: var(--border)">
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div> for _, user := range users {
if user.ID == currentUser.ID { <tr id={ "user-" + user.ID } class="transition-colors hover:bg-surface-hover">
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span> <!-- Username -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-2">
<span class="grid place-items-center h-8 w-8 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("user", "h-4 w-4")
</span>
<div>
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div>
if user.ID == currentUser.ID {
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">You</span>
}
</div>
</div>
</td>
<!-- Email -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div>
</td>
<!-- Role Toggle (with last-admin protection) -->
<td class="px-6 py-4 whitespace-nowrap">
if user.Role == "admin" && adminCount == 1 {
<!-- Last admin - disabled -->
<div class="relative">
<select
disabled
class="input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed"
title="Cannot demote the last admin"
>
<option value="user">User</option>
<option value="admin" selected>Admin</option>
</select>
</div>
} else {
<select
hx-put={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#role-result-" + user.ID }
hx-swap="innerHTML"
name="role"
class="input w-auto py-1 pr-7 text-xs"
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
hx-trigger="change"
hx-vals='{"role": this.value}'
>
<option value="user" selected?={ user.Role == "user" }>User</option>
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
</select>
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
}
</td>
<!-- Created -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
</td>
<!-- Actions -->
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="inline-flex items-center gap-1">
<button
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
hx-target="#modal-container"
hx-swap="innerHTML"
class="btn btn-secondary text-xs px-2.5 py-1"
>
@Icon("edit", "h-4 w-4")
Edit
</button>
if user.Role == "admin" && adminCount == 1 {
<button
disabled
class="btn btn-danger text-xs px-2.5 py-1"
title="Cannot delete the last admin"
>
@Icon("trash", "h-4 w-4")
Delete
</button>
} else {
<button
hx-delete={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#user-" + user.ID }
hx-swap="outerHTML swap:0.5s"
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
class="btn btn-danger text-xs px-2.5 py-1"
>
@Icon("trash", "h-4 w-4")
Delete
</button>
} }
</div> </div>
</div> </td>
</td> </tr>
<!-- Email --> }
<td class="px-6 py-4 whitespace-nowrap"> </tbody>
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div> </table>
</td> </div>
<!-- Role Toggle (with last-admin protection) -->
<td class="px-6 py-4 whitespace-nowrap">
if user.Role == "admin" && adminCount == 1 {
<!-- Last admin - disabled -->
<div class="relative">
<select
disabled
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
title="Cannot demote the last admin"
>
<option value="user">User</option>
<option value="admin" selected>Admin</option>
</select>
</div>
} else {
<select
hx-put={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#role-result-" + user.ID }
hx-swap="innerHTML"
name="role"
class="text-sm rounded px-2 py-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
hx-trigger="change"
hx-vals='{"role": this.value}'
>
<option value="user" selected?={ user.Role == "user" }>User</option>
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
</select>
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
}
</td>
<!-- Created -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
</td>
<!-- Actions -->
<td class="px-6 py-4 whitespace-nowrap text-right">
<button
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
hx-target="#modal-container"
hx-swap="innerHTML"
class="text-sm px-3 py-1 rounded mr-2"
style="background-color: var(--accent); color: white;"
>
Edit
</button>
if user.Role == "admin" && adminCount == 1 {
<button
disabled
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50"
style="background-color: #dc2626; color: white;"
title="Cannot delete the last admin"
>
Delete
</button>
} else {
<button
hx-delete={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#user-" + user.ID }
hx-swap="outerHTML swap:0.5s"
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
class="text-sm px-3 py-1 rounded"
style="background-color: #dc2626; color: white;"
>
Delete
</button>
}
</td>
</tr>
}
</tbody>
</table>
</div> </div>
</main> </main>
</div> </div>
+83 -43
View File
@@ -37,7 +37,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><div class=\"flex\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,207 +45,247 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"w-full\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">User Management</h1><p style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card rounded-lg border overflow-hidden\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-medium uppercase tracking-wider\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("users", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">User Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card overflow-hidden\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, user := range users { for _, user := range users {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("user-" + user.ID) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("user-" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 37, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 43, Col: 36}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"hover:bg-opacity-50\" style=\"transition: background-color 0.2s;\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center\"><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"transition-colors hover:bg-surface-hover\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center gap-2\"><span class=\"grid place-items-center h-8 w-8 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("user", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 42, Col: 96} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 51, Col: 97}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.ID == currentUser.ID { if user.ID == currentUser.ID {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"text-xs px-2 py-1 rounded\" style=\"background-color: var(--accent); color: white;\">You</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">You</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 51, Col: 79} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 60, Col: 80}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Role == "admin" && adminCount == 1 { if user.Role == "admin" && adminCount == 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"text-sm rounded px-2 py-1 cursor-not-allowed opacity-50\" style=\"background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<select hx-put=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<select hx-put=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 70, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 78, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("#role-result-" + user.ID) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("#role-result-" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 72, Col: 50} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 80, Col: 51}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"text-sm rounded px-2 py-1\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);\" onchange=\"this.dispatchEvent(new Event('htmx:trigger'))\" hx-trigger=\"change\" hx-vals='{\"role\": this.value}'><option value=\"user\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"input w-auto py-1 pr-7 text-xs\" onchange=\"this.dispatchEvent(new Event('htmx:trigger'))\" hx-trigger=\"change\" hx-vals='{\"role\": this.value}'><option value=\"user\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Role == "user" { if user.Role == "user" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">User</option> <option value=\"admin\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user.Role == "admin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Admin</option></select><div id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">User</option> <option value=\"admin\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if user.Role == "admin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Admin</option></select><div id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("role-result-" + user.ID) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("role-result-" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 84, Col: 46} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 91, Col: 47}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" class=\"text-xs mt-1\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-xs mt-1\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone)) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 89, Col: 125} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 96, Col: 126}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><button hx-get=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><div class=\"inline-flex items-center gap-1\"><button hx-get=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/users/" + user.ID + "/profile-modal") templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/users/" + user.ID + "/profile-modal")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 94, Col: 65} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 102, Col: 67}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"text-sm px-3 py-1 rounded mr-2\" style=\"background-color: var(--accent); color: white;\">Edit</button> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Edit</button> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Role == "admin" && adminCount == 1 { if user.Role == "admin" && adminCount == 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<button disabled class=\"text-sm px-3 py-1 rounded cursor-not-allowed opacity-50\" style=\"background-color: #dc2626; color: white;\" title=\"Cannot delete the last admin\">Delete</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button disabled class=\"btn btn-danger text-xs px-2.5 py-1\" title=\"Cannot delete the last admin\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Delete</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button hx-delete=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<button hx-delete=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 113, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 121, Col: 57}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue("#user-" + user.ID) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue("#user-" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 115, Col: 43} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 123, Col: 45}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"text-sm px-3 py-1 rounded\" style=\"background-color: #dc2626; color: white;\">Delete</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></td></tr>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</tbody></table></div></main></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></div></main></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+35 -38
View File
@@ -12,78 +12,75 @@ templ Analytics(user User) {
</head> </head>
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }"> <body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
@Header(user, "/analytics") @Header(user, "/analytics")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">📊 Reading Analytics</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Track your reading habits and device usage</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("chart", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Reading Analytics</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Track your reading habits and device usage</p>
</div> </div>
<!-- Date Range Picker --> <div class="card p-4 mb-6">
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="flex flex-wrap gap-4 items-end">
<div class="flex flex-wrap gap-4 items-center">
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Start Date</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Start Date</label>
<input <input
type="date" type="date"
id="start-date" id="start-date"
onchange="loadAnalytics()" onchange="loadAnalytics()"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">End Date</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">End Date</label>
<input <input
type="date" type="date"
id="end-date" id="end-date"
onchange="loadAnalytics()" onchange="loadAnalytics()"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div class="flex items-end"> <div>
<button @click="loadAnalytics()" class="btn-primary px-6 py-2 rounded-lg">Update</button> <button @click="loadAnalytics()" class="btn btn-primary">Update</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Stats Cards --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8" id="stats-container">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8" id="stats-container"> <div class="stat-card">
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);"> <h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Books Read</h3>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Books Read</h3> <p id="total-books" class="text-3xl font-bold" style="color: var(--accent);">-</p>
<p id="total-books" class="text-3xl font-bold" style="color: var(--text-primary);">-</p>
</div> </div>
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card">
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Pages Read</h3> <h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Pages Read</h3>
<p id="total-pages" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="total-pages" class="text-3xl font-bold" style="color: var(--accent);">-</p>
</div> </div>
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card">
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Reading Time</h3> <h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Reading Time</h3>
<p id="reading-time" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="reading-time" class="text-3xl font-bold" style="color: var(--accent);">-</p>
</div> </div>
<div class="card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card">
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Completion Rate</h3> <h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Completion Rate</h3>
<p id="completion-rate" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="completion-rate" class="text-3xl font-bold" style="color: var(--accent);">-</p>
</div> </div>
</div> </div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Daily Reading Chart --> <div class="card p-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Daily Reading Minutes</h3>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Daily Reading Minutes</h3>
<div class="h-80"> <div class="h-80">
<canvas id="daily-reading-chart"></canvas> <canvas id="daily-reading-chart"></canvas>
</div> </div>
</div> </div>
<!-- Device Usage Chart --> <div class="card p-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Device Usage</h3>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Device Usage</h3>
<div class="h-80"> <div class="h-80">
<canvas id="device-usage-chart"></canvas> <canvas id="device-usage-chart"></canvas>
</div> </div>
</div> </div>
</div> </div>
<!-- Popular Books --> <div class="card p-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary);">Most Read Books</h3>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary);">Most Read Books</h3>
<div id="popular-books" class="space-y-3"> <div id="popular-books" class="space-y-3">
<div class="text-center py-8" style="color: var(--text-secondary);"> <div class="text-center py-8" style="color: var(--text-secondary);">
<div class="loading-spinner mx-auto mb-4"></div> <div class="loading-spinner mx-auto mb-4"></div>
+9 -1
View File
@@ -37,7 +37,15 @@ func Analytics(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">📊 Reading Analytics</h1><p style=\"color: var(--text-secondary)\">Track your reading habits and device usage</p></div><!-- Date Range Picker --><div class=\"card p-4 rounded-lg border mb-6\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary);\">Start Date</label> <input type=\"date\" id=\"start-date\" onchange=\"loadAnalytics()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary);\">End Date</label> <input type=\"date\" id=\"end-date\" onchange=\"loadAnalytics()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div class=\"flex items-end\"><button @click=\"loadAnalytics()\" class=\"btn-primary px-6 py-2 rounded-lg\">Update</button></div></div></div><!-- Stats Cards --><div class=\"grid grid-cols-1 md:grid-cols-4 gap-6 mb-8\" id=\"stats-container\"><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Books Read</h3><p id=\"total-books\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Pages Read</h3><p id=\"total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Reading Time</h3><p id=\"reading-time\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div><div class=\"card p-6 rounded-lg border transition-all duration-200 ease hover:-translate-y-0.5 hover:shadow-lg\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-secondary);\">Completion Rate</h3><p id=\"completion-rate\" class=\"text-3xl font-bold\" style=\"color: var(--text-primary);\">-</p></div></div><!-- Charts Row --><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8\"><!-- Daily Reading Chart --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Daily Reading Minutes</h3><div class=\"h-80\"><canvas id=\"daily-reading-chart\"></canvas></div></div><!-- Device Usage Chart --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Device Usage</h3><div class=\"h-80\"><canvas id=\"device-usage-chart\"></canvas></div></div></div><!-- Popular Books --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary);\">Most Read Books</h3><div id=\"popular-books\" class=\"space-y-3\"><div class=\"text-center py-8\" style=\"color: var(--text-secondary);\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading analytics...</p></div></div></div></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chart", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Reading Analytics</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Track your reading habits and device usage</p></div><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-end\"><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Start Date</label> <input type=\"date\" id=\"start-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div class=\"flex-1 min-w-[200px]\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">End Date</label> <input type=\"date\" id=\"end-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div><button @click=\"loadAnalytics()\" class=\"btn btn-primary\">Update</button></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8\" id=\"stats-container\"><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Books Read</h3><p id=\"total-books\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Pages Read</h3><p id=\"total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Reading Time</h3><p id=\"reading-time\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary);\">Completion Rate</h3><p id=\"completion-rate\" class=\"text-3xl font-bold\" style=\"color: var(--accent);\">-</p></div></div><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Daily Reading Minutes</h3><div class=\"h-80\"><canvas id=\"daily-reading-chart\"></canvas></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Device Usage</h3><div class=\"h-80\"><canvas id=\"device-usage-chart\"></canvas></div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary);\">Most Read Books</h3><div id=\"popular-books\" class=\"space-y-3\"><div class=\"text-center py-8\" style=\"color: var(--text-secondary);\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading analytics...</p></div></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+82 -49
View File
@@ -2,64 +2,97 @@ package templates
templ APIExplorer(explorer APIExplorerData) { templ APIExplorer(explorer APIExplorerData) {
if !explorer.IsLoggedIn { if !explorer.IsLoggedIn {
// Show mode toggle and mock data <div x-data="devices" class="card p-6 mt-8 space-y-5">
<div x-data="devices" class="border border-border rounded-lg p-6 mt-8 bg-background-secondary"> <div class="flex items-center gap-2 flex-wrap">
<div class="flex gap-2 mb-4"> if explorer.Endpoint.Method == "GET" {
<button class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer bg-accent text-white">Mock Data</button> <span class="badge status-completed">{ explorer.Endpoint.Method }</span>
<button class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button> } else if explorer.Endpoint.Method == "DELETE" {
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
} else if explorer.Endpoint.Method == "POST" {
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
} else {
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
}
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
</div> </div>
<div class="mb-4"> <div class="flex gap-2">
<div class="mb-4"> <button class="btn btn-primary">Mock Data</button>
<select disabled class="w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent"> <button class="btn btn-secondary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button>
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
</select>
</div>
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body (Example)</h4>
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code class="text-text-primary text-sm">{ explorer.Endpoint.RequestBody }</code></pre>
</div> </div>
<div class="mt-4"> <div>
<h4 class="text-text-secondary mb-2 text-sm font-medium">Response (Mock)</h4> <p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body (Example)</p>
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code class="text-text-primary text-sm">{ explorer.Endpoint.Response }</code></pre> <pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.RequestBody }</code></pre>
</div> </div>
<div class="flex gap-2 mt-4"> <div>
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Request</button> <p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Response (Mock)</p>
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Response</button> <pre class="card p-4 overflow-x-auto"><code class="text-sm" style="color: var(--text-primary);">{ explorer.Endpoint.Response }</code></pre>
</div>
<div class="flex gap-2">
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))" class="btn btn-secondary text-sm">
@Icon("copy", "h-4 w-4")
Copy Request
</button>
<button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))" class="btn btn-secondary text-sm">
@Icon("copy", "h-4 w-4")
Copy Response
</button>
</div> </div>
</div> </div>
} else { } else {
// Show interactive explorer with real execution <div class="card p-6 mt-8 space-y-5" x-data="apiExplorerDoc" x-init="initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')">
<div class="border border-border rounded-lg p-6 mt-8 bg-background-secondary" x-data="apiExplorerDoc" x-init="initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')"> <div class="flex items-center gap-2 flex-wrap">
<div class="flex gap-2 mb-4"> if explorer.Endpoint.Method == "GET" {
<button class="mode-btn px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer hover:bg-background-secondary text-sm" id="mock-btn" @click="showDocMode('mock')">Mock Data</button> <span class="badge status-completed">{ explorer.Endpoint.Method }</span>
<button class="mode-btn px-4 py-2 bg-accent text-white rounded cursor-pointer text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button> } else if explorer.Endpoint.Method == "DELETE" {
<span class="badge status-failed">{ explorer.Endpoint.Method }</span>
} else if explorer.Endpoint.Method == "POST" {
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ explorer.Endpoint.Method }</span>
} else {
<span class="badge status-processing">{ explorer.Endpoint.Method }</span>
}
<code class="text-sm" style="color: var(--text-secondary);">{ explorer.Endpoint.Path }</code>
</div> </div>
<div class="mb-4"> <div class="flex gap-2">
<div class="mb-4"> <button class="mode-btn btn btn-secondary text-sm" id="mock-btn" @click="showDocMode('mock')">Mock Data</button>
<select id="http-method" class="w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent"> <button class="mode-btn btn btn-primary text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button>
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option> </div>
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option> <div>
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option> <label for="http-method" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Method</label>
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option> <select id="http-method" class="input">
</select> <option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
</select>
</div>
<div>
<label for="request-body" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary);">Request Body</label>
<textarea id="request-body" placeholder="Edit request body…" class="input min-h-[150px] font-mono">{ explorer.Endpoint.RequestBody }</textarea>
</div>
<button id="try-it-out" @click="tryDocEndpoint()" class="btn btn-primary">
@Icon("play", "h-4 w-4")
Try It Out
</button>
<div class="api-response hidden space-y-2">
<div class="flex justify-between text-sm">
<span id="response-status" class="font-semibold" style="color: var(--text-primary);"></span>
<span id="response-time" style="color: var(--text-secondary);"></span>
</div> </div>
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body</h4> <pre class="card p-4 overflow-x-auto"><code id="response-body" class="text-sm" style="color: var(--text-primary);"></code></pre>
<textarea id="request-body" placeholder="Edit request body..." class="w-full min-h-[150px] px-3 py-2 font-mono text-sm bg-background-primary text-text-primary border border-border rounded focus:outline-none focus:ring-2 focus:ring-accent">{ explorer.Endpoint.RequestBody }</textarea>
</div> </div>
<button id="try-it-out" @click="tryDocEndpoint()" class="bg-accent text-white px-6 py-3 rounded cursor-pointer mt-4 hover:opacity-90">Try It Out</button> <div class="flex gap-2 flex-wrap">
<div class="api-response mt-4 hidden"> <button @click="copyDocRequest()" class="btn btn-secondary text-sm">
<div class="flex justify-between mb-2 text-sm"> @Icon("copy", "h-4 w-4")
<span id="response-status" class="font-semibold"></span> Copy Request
<span id="response-time" class="text-text-secondary"></span> </button>
</div> <button @click="copyDocResponse()" class="btn btn-secondary text-sm">
<pre class="bg-background-primary p-4 rounded-lg overflow-x-auto"><code id="response-body" class="text-text-primary text-sm"></code></pre> @Icon("copy", "h-4 w-4")
</div> Copy Response
<div class="flex gap-2 mt-4 flex-wrap"> </button>
<button @click="copyDocRequest()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Request</button> <button @click="generateDocCURL()" class="btn btn-secondary text-sm">
<button @click="copyDocResponse()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Copy Response</button> @Icon("external", "h-4 w-4")
<button @click="generateDocCURL()" class="px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm">Generate cURL</button> Generate cURL
</button>
</div> </div>
</div> </div>
} }
+238 -50
View File
@@ -30,131 +30,319 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if !explorer.IsLoggedIn { if !explorer.IsLoggedIn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <div x-data=\"devices\" class=\"border border-border rounded-lg p-6 mt-8 bg-background-secondary\"><div class=\"flex gap-2 mb-4\"><button class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer bg-accent text-white\">Mock Data</button> <button class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary opacity-50 cursor-not-allowed\" disabled>Login to Try Real</button></div><div class=\"mb-4\"><div class=\"mb-4\"><select disabled class=\"w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent\"><option value=\"GET\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"devices\" class=\"card p-6 mt-8 space-y-5\"><div class=\"flex items-center gap-2 flex-wrap\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "GET" { if explorer.Endpoint.Method == "GET" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"badge status-completed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 8, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if explorer.Endpoint.Method == "DELETE" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"badge status-failed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 10, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if explorer.Endpoint.Method == "POST" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 12, Col: 120}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"badge status-processing\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 14, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">GET</option> <option value=\"POST\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<code class=\"text-sm\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "POST" { var templ_7745c5c3_Var6 string
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected") templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 16, Col: 88}
}
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">POST</option> <option value=\"PUT\"") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "PUT" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</code></div><div class=\"flex gap-2\"><button class=\"btn btn-primary\">Mock Data</button> <button class=\"btn btn-secondary opacity-50 cursor-not-allowed\" disabled>Login to Try Real</button></div><div><p class=\"text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Request Body (Example)</p><pre class=\"card p-4 overflow-x-auto\"><code class=\"text-sm\" style=\"color: var(--text-primary);\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">PUT</option> <option value=\"DELETE\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "DELETE" { var templ_7745c5c3_Var7 string
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected") templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 24, Col: 131}
}
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">DELETE</option></select></div><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Request Body (Example)</h4><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code class=\"text-text-primary text-sm\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</code></pre></div><div><p class=\"text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Response (Mock)</p><pre class=\"card p-4 overflow-x-auto\"><code class=\"text-sm\" style=\"color: var(--text-primary);\">")
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 21, Col: 141}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</code></pre></div><div class=\"mt-4\"><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Response (Mock)</h4><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code class=\"text-text-primary text-sm\">") var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 28, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</code></pre></div><div class=\"flex gap-2\"><button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))\" class=\"btn btn-secondary text-sm\">")
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 25, Col: 138}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</code></pre></div><div class=\"flex gap-2 mt-4\"><button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Request</button> <button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Response</button></div></div>") templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Copy Request</button> <button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Copy Response</button></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " <div class=\"border border-border rounded-lg p-6 mt-8 bg-background-secondary\" x-data=\"apiExplorerDoc\" x-init=\"initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')\"><div class=\"flex gap-2 mb-4\"><button class=\"mode-btn px-4 py-2 border border-border rounded bg-background-primary text-text-primary cursor-pointer hover:bg-background-secondary text-sm\" id=\"mock-btn\" @click=\"showDocMode('mock')\">Mock Data</button> <button class=\"mode-btn px-4 py-2 bg-accent text-white rounded cursor-pointer text-sm\" id=\"real-btn\" @click=\"showDocMode('real')\">Try It Out</button></div><div class=\"mb-4\"><div class=\"mb-4\"><select id=\"http-method\" class=\"w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent\"><option value=\"GET\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-6 mt-8 space-y-5\" x-data=\"apiExplorerDoc\" x-init=\"initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')\"><div class=\"flex items-center gap-2 flex-wrap\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "GET" { if explorer.Endpoint.Method == "GET" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"badge status-completed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 45, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if explorer.Endpoint.Method == "DELETE" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"badge status-failed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 47, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if explorer.Endpoint.Method == "POST" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 120}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"badge status-processing\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 51, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">GET</option> <option value=\"POST\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<code class=\"text-sm\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 53, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</code></div><div class=\"flex gap-2\"><button class=\"mode-btn btn btn-secondary text-sm\" id=\"mock-btn\" @click=\"showDocMode('mock')\">Mock Data</button> <button class=\"mode-btn btn btn-primary text-sm\" id=\"real-btn\" @click=\"showDocMode('real')\">Try It Out</button></div><div><label for=\"http-method\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Method</label> <select id=\"http-method\" class=\"input\"><option value=\"GET\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if explorer.Endpoint.Method == "GET" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ">GET</option> <option value=\"POST\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "POST" { if explorer.Endpoint.Method == "POST" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">POST</option> <option value=\"PUT\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">POST</option> <option value=\"PUT\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "PUT" { if explorer.Endpoint.Method == "PUT" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">PUT</option> <option value=\"DELETE\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">PUT</option> <option value=\"DELETE\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if explorer.Endpoint.Method == "DELETE" { if explorer.Endpoint.Method == "DELETE" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, ">DELETE</option></select></div><h4 class=\"text-text-secondary mb-2 text-sm font-medium\">Request Body</h4><textarea id=\"request-body\" placeholder=\"Edit request body...\" class=\"w-full min-h-[150px] px-3 py-2 font-mono text-sm bg-background-primary text-text-primary border border-border rounded focus:outline-none focus:ring-2 focus:ring-accent\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">DELETE</option></select></div><div><label for=\"request-body\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary);\">Request Body</label> <textarea id=\"request-body\" placeholder=\"Edit request body\" class=\"input min-h-[150px] font-mono\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 274} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 70, Col: 136}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</textarea></div><button id=\"try-it-out\" @click=\"tryDocEndpoint()\" class=\"bg-accent text-white px-6 py-3 rounded cursor-pointer mt-4 hover:opacity-90\">Try It Out</button><div class=\"api-response mt-4 hidden\"><div class=\"flex justify-between mb-2 text-sm\"><span id=\"response-status\" class=\"font-semibold\"></span> <span id=\"response-time\" class=\"text-text-secondary\"></span></div><pre class=\"bg-background-primary p-4 rounded-lg overflow-x-auto\"><code id=\"response-body\" class=\"text-text-primary text-sm\"></code></pre></div><div class=\"flex gap-2 mt-4 flex-wrap\"><button @click=\"copyDocRequest()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Request</button> <button @click=\"copyDocResponse()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Copy Response</button> <button @click=\"generateDocCURL()\" class=\"px-4 py-2 border border-border rounded bg-background-primary text-text-primary hover:bg-background-secondary cursor-pointer text-sm\">Generate cURL</button></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</textarea></div><button id=\"try-it-out\" @click=\"tryDocEndpoint()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Try It Out</button><div class=\"api-response hidden space-y-2\"><div class=\"flex justify-between text-sm\"><span id=\"response-status\" class=\"font-semibold\" style=\"color: var(--text-primary);\"></span> <span id=\"response-time\" style=\"color: var(--text-secondary);\"></span></div><pre class=\"card p-4 overflow-x-auto\"><code id=\"response-body\" class=\"text-sm\" style=\"color: var(--text-primary);\"></code></pre></div><div class=\"flex gap-2 flex-wrap\"><button @click=\"copyDocRequest()\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Copy Request</button> <button @click=\"copyDocResponse()\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("copy", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Copy Response</button> <button @click=\"generateDocCURL()\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("external", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "Generate cURL</button></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+182 -230
View File
@@ -8,50 +8,52 @@ import (
templ ProgressSyncModal(user User, book handlers.MediaDetail) { templ ProgressSyncModal(user User, book handlers.MediaDetail) {
<div <div
id="progress-sync-modal" id="progress-sync-modal"
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
style="background-color: rgba(0, 0, 0, 0.7);" style="background-color: var(--surface-overlay);"
> >
<div <div
class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8" class="card w-full max-w-4xl my-8 p-6"
style="background-color: var(--bg-secondary); border-color: var(--border);" style="box-shadow: var(--shadow-pop);"
> >
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<div> <div class="flex items-center gap-3">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Progress</h2> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p> @Icon("sync", "h-5 w-5")
</span>
<div>
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Sync Progress</h2>
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p>
</div>
</div> </div>
<button <button
@click="hideProgressSyncModal()" @click="hideProgressSyncModal()"
class="p-2 hover:opacity-80 rounded-lg" class="icon-btn"
style="color: var(--text-primary); background-color: var(--bg-primary);" aria-label="Close"
> >
@Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
if book.ActiveConflict != nil { if book.ActiveConflict != nil {
<div <div
class="mb-6 p-4 rounded-lg border" class="mb-6 p-4 rounded-xl flex items-start gap-3"
style="background-color: #f59e0b20; border-color: #f59e0b;" style="background-color: color-mix(in srgb, var(--status-warning) 15%, transparent); border: 1px solid var(--status-warning);"
> >
<p class="font-semibold mb-2" style="color: var(--text-primary);"> <span class="shrink-0 mt-0.5" style="color: var(--status-warning);">@Icon("alert", "h-5 w-5")</span>
⚠️ Conflict Detected <div>
</p> <p class="font-semibold mb-1" style="color: var(--text-primary);">
<p class="text-sm" style="color: var(--text-secondary);"> Conflict Detected
Progress differs between devices. Choose which version to keep. </p>
</p> <p class="text-sm" style="color: var(--text-secondary);">
Progress differs between devices. Choose which version to keep.
</p>
</div>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
for source, data := range book.ActiveConflict.ConflictData { for source, data := range book.ActiveConflict.ConflictData {
<div <div class="card p-4">
class="card p-4 rounded-lg border"
style="background-color: var(--bg-primary); border-color: var(--border);"
>
<div class="flex justify-between items-start mb-3"> <div class="flex justify-between items-start mb-3">
<div> <div>
<span <span class="badge mb-2" style="background-color: var(--accent); color: #fff;">
class="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2"
style="background-color: var(--accent); color: white;"
>
{ data.Source } { data.Source }
</span> </span>
<p class="text-xs" style="color: var(--text-secondary);"> <p class="text-xs" style="color: var(--text-secondary);">
@@ -83,8 +85,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
<div class="mt-3"> <div class="mt-3">
<button <button
@click={"resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')"} @click={"resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')"}
class="px-3 py-1.5 rounded-lg text-sm font-semibold" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
Keep This Keep This
</button> </button>
@@ -96,10 +97,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
<div class="mb-4" style="color: var(--text-secondary);"> <div class="mb-4" style="color: var(--text-secondary);">
<p>No conflicts detected. Current progress from <strong>{ book.ReadingProgress.LastSyncSource.String }</strong>:</p> <p>No conflicts detected. Current progress from <strong>{ book.ReadingProgress.LastSyncSource.String }</strong>:</p>
</div> </div>
<div <div class="card p-6 text-center">
class="card p-6 rounded-lg border text-center"
style="background-color: var(--bg-primary); border-color: var(--border);"
>
<div class="text-5xl font-bold mb-4" style="color: var(--accent);"> <div class="text-5xl font-bold mb-4" style="color: var(--accent);">
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }% { fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
</div> </div>
@@ -121,8 +119,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
<div class="flex justify-end space-x-3 mt-6"> <div class="flex justify-end space-x-3 mt-6">
<button <button
@click="hideProgressSyncModal()" @click="hideProgressSyncModal()"
class="px-4 py-2 rounded-lg" class="btn btn-secondary"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
> >
Close Close
</button> </button>
@@ -134,14 +131,16 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
templ NotesHighlightsModal(book handlers.MediaDetail) { templ NotesHighlightsModal(book handlers.MediaDetail) {
<div <div
id="notes-modal" id="notes-modal"
class="hidden fixed inset-0 z-50 flex items-center justify-center" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"
style="background-color: rgba(0, 0, 0, 0.7);" style="background-color: var(--surface-overlay);"
> >
<div <div
class="card rounded-lg p-8 w-full max-w-2xl mx-4 text-center" class="card w-full max-w-2xl p-8 text-center"
style="background-color: var(--bg-secondary); border-color: var(--border);" style="box-shadow: var(--shadow-pop);"
> >
<div class="text-6xl mb-4">📝</div> <span class="inline-grid place-items-center h-14 w-14 rounded-2xl mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("edit", "h-7 w-7")
</span>
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Notes & Highlights</h2> <h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Notes & Highlights</h2>
<p class="mb-2" style="color: var(--text-secondary);"> <p class="mb-2" style="color: var(--text-secondary);">
This book has <strong>{ book.NotesCount }</strong> notes and <strong>{ book.HighlightsCount }</strong> highlights. This book has <strong>{ book.NotesCount }</strong> notes and <strong>{ book.HighlightsCount }</strong> highlights.
@@ -150,8 +149,7 @@ templ NotesHighlightsModal(book handlers.MediaDetail) {
<div> <div>
<button <button
@click="hideNotesModal()" @click="hideNotesModal()"
class="px-6 py-2 rounded-lg font-semibold" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
Close Close
</button> </button>
@@ -163,27 +161,31 @@ templ NotesHighlightsModal(book handlers.MediaDetail) {
templ MetadataEditorModal(book handlers.MediaDetail) { templ MetadataEditorModal(book handlers.MediaDetail) {
<div <div
id="metadata-editor-modal" id="metadata-editor-modal"
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
style="background-color: rgba(0, 0, 0, 0.7);" style="background-color: var(--surface-overlay);"
> >
<div <div
class="rounded-lg w-full max-w-5xl mx-4 my-8 flex flex-col max-h-[90vh]" class="card w-full max-w-5xl my-8 flex flex-col max-h-[90vh]"
style="background-color: var(--bg-secondary); border: 1px solid var(--border);" style="box-shadow: var(--shadow-pop);"
> >
<div class="flex justify-between items-center p-6 border-b flex-shrink-0" style="border-color: var(--border);"> <div class="flex justify-between items-center p-6 border-b flex-shrink-0" style="border-color: var(--border);">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2> <div class="flex items-center gap-3">
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("edit", "h-5 w-5")
</span>
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2>
</div>
<button <button
@click="hideMetadataEditor()" @click="hideMetadataEditor()"
class="p-2 hover:opacity-80 rounded-lg" class="icon-btn"
style="color: var(--text-primary); background-color: var(--bg-primary);" aria-label="Close"
> >
@Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
<div class="flex flex-col md:flex-row gap-8 p-6 flex-1 min-h-0 overflow-y-auto"> <div class="flex flex-col md:flex-row gap-8 p-6 flex-1 min-h-0 overflow-y-auto">
<!-- Cover Section (Left) -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="w-64 h-96 rounded-lg overflow-hidden shadow-lg mb-3 cursor-pointer relative group" <div class="w-64 h-96 rounded-xl overflow-hidden mb-3 cursor-pointer relative group"
style="background-color: var(--bg-primary);" style="background-color: var(--bg-primary);"
@click="document.getElementById('cover-upload-input').click()" @click="document.getElementById('cover-upload-input').click()"
> >
@@ -209,27 +211,26 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
<div class="w-64 space-y-2"> <div class="w-64 space-y-2">
<button <button
type="button" type="button"
class="w-full px-3 py-2 text-sm rounded-lg font-semibold" class="btn btn-primary w-full"
style="background-color: var(--accent); color: white;"
@click="generateCover()" @click="generateCover()"
x-show="coverGenerating" x-show="coverGenerating"
disabled disabled
> >
@Icon("refresh", "h-4 w-4")
Generating... Generating...
</button> </button>
<button <button
type="button" type="button"
class="w-full px-3 py-2 text-sm rounded-lg font-semibold" class="btn btn-primary w-full"
style="background-color: var(--accent); color: white;"
@click="generateCover()" @click="generateCover()"
x-show="!coverGenerating" x-show="!coverGenerating"
> >
@Icon("refresh", "h-4 w-4")
Generate Cover Generate Cover
</button> </button>
<button <button
type="button" type="button"
class="w-full px-3 py-2 text-sm rounded-lg" class="btn btn-ghost w-full"
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
@click="removeCover()" @click="removeCover()"
x-show="hasExistingCover || newCoverPreview" x-show="hasExistingCover || newCoverPreview"
> >
@@ -237,53 +238,43 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</button> </button>
</div> </div>
</div> </div>
<!-- Accordion Fields (Right) -->
<div class="flex-1 min-w-0 space-y-2"> <div class="flex-1 min-w-0 space-y-2">
<!-- Basic Info --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('basic')"> @click="toggleSection('basic')">
<span class="font-semibold">Basic Info</span> <span class="font-semibold">Basic Info</span>
<span x-text="openSections.basic ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.basic">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.basic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.basic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.basic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Title</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Title</label>
<input type="text" name="title" value={ book.Title } <input type="text" name="title" value={ book.Title } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Author</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Author</label>
<input type="text" name="author" value={ textToString(book.Author) } <input type="text" name="author" value={ textToString(book.Author) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Description</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Description</label>
<textarea name="description" rows="3" <textarea name="description" rows="3" class="input"
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
>{ textToString(book.Description) }</textarea> >{ textToString(book.Description) }</textarea>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Summary</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Summary</label>
<textarea name="summary" rows="2" <textarea name="summary" rows="2" class="input"
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
>{ textToString(book.Summary) }</textarea> >{ textToString(book.Summary) }</textarea>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Tags</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Tags</label>
<div class="flex flex-wrap gap-1.5 mb-2"> <div class="flex flex-wrap gap-1.5 mb-2">
<template x-for="(tag, idx) in editorTags" :key="idx"> <template x-for="(tag, idx) in editorTags" :key="idx">
<span <span class="chip">
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium"
style="background-color: var(--accent); color: white;"
>
<span x-text="tag"></span> <span x-text="tag"></span>
<button type="button" class="hover:opacity-70 leading-none" @click="removeEditorTag(idx)"></button> <button type="button" class="hover:bg-surface-hover rounded leading-none" @click="removeEditorTag(idx)">@Icon("close", "h-3 w-3")</button>
</span> </span>
</template> </template>
</div> </div>
@@ -297,20 +288,19 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
@keydown="onTagKeydown($event)" @keydown="onTagKeydown($event)"
@blur="hideEditorTagDropdown()" @blur="hideEditorTagDropdown()"
placeholder="Add tag..." placeholder="Add tag..."
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
<div <div
x-show="showTagDropdown" x-show="showTagDropdown"
x-transition x-transition
class="mt-1 w-full rounded-lg shadow-lg border max-h-48 overflow-y-auto" x-cloak
style="background-color: var(--bg-primary); border-color: var(--border);" class="mt-1 w-full rounded-lg border max-h-48 overflow-y-auto"
style="background-color: var(--bg-secondary); border-color: var(--border); box-shadow: var(--shadow-card);"
> >
<template x-for="sug in tagSuggestions" :key="sug.value"> <template x-for="sug in tagSuggestions" :key="sug.value">
<button <button
type="button" type="button"
class="w-full text-left px-3 py-2 text-sm flex justify-between items-center" class="w-full text-left px-3 py-2 text-sm flex justify-between items-center hover:bg-surface-hover"
:class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''" :class="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'bg-surface-hover' : ''"
:style="highlightedTagIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'"
@click="selectEditorTagSuggestion(sug.value)" @click="selectEditorTagSuggestion(sug.value)"
> >
<span x-text="sug.value"></span> <span x-text="sug.value"></span>
@@ -321,164 +311,142 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div> </div>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Community Rating (0-10)</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Community Rating (0-10)</label>
<input type="number" name="community_rating" min="0" max="10" step="0.1" <input type="number" name="community_rating" min="0" max="10" step="0.1"
value={ fmt.Sprintf("%.1f", book.CommunityRating.Float64) } value={ fmt.Sprintf("%.1f", book.CommunityRating.Float64) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Publication --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('publication')"> @click="toggleSection('publication')">
<span class="font-semibold">Publication</span> <span class="font-semibold">Publication</span>
<span x-text="openSections.publication ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.publication">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.publication" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.publication" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.publication" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Publisher</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Publisher</label>
<input type="text" name="publisher" value={ textToString(book.Publisher) } <input type="text" name="publisher" value={ textToString(book.Publisher) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Date Published</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Date Published</label>
<input type="date" name="date_published" <input type="date" name="date_published"
value={ formatDateForInput(book.DatePublished) } value={ formatDateForInput(book.DatePublished) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Edition</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Edition</label>
<input type="text" name="edition" value={ textToString(book.Edition) } <input type="text" name="edition" value={ textToString(book.Edition) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Language</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Language</label>
<input type="text" name="language" value={ textToString(book.Language) } <input type="text" name="language" value={ textToString(book.Language) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Genre</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Genre</label>
<input type="text" name="genre" value={ textToString(book.Genre) } <input type="text" name="genre" value={ textToString(book.Genre) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Copyright Year</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Copyright Year</label>
<input type="number" name="copyright_year" <input type="number" name="copyright_year"
value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) } value={ fmt.Sprintf("%d", book.CopyrightYear.Int32) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Series --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('series')"> @click="toggleSection('series')">
<span class="font-semibold">Series</span> <span class="font-semibold">Series</span>
<span x-text="openSections.series ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.series">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.series" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.series" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.series" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Series</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Series</label>
<input type="text" name="series" value={ textToString(book.Series) } <input type="text" name="series" value={ textToString(book.Series) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div class="grid grid-cols-3 gap-3"> <div class="grid grid-cols-3 gap-3">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Number</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Number</label>
<input type="number" name="series_number" <input type="number" name="series_number"
value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) } value={ fmt.Sprintf("%d", book.SeriesNumber.Int32) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Count</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Count</label>
<input type="number" name="series_count" <input type="number" name="series_count"
value={ fmt.Sprintf("%d", book.SeriesCount.Int32) } value={ fmt.Sprintf("%d", book.SeriesCount.Int32) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Volume</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Volume</label>
<input type="number" name="volume" <input type="number" name="volume"
value={ fmt.Sprintf("%d", book.Volume.Int32) } value={ fmt.Sprintf("%d", book.Volume.Int32) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Identifiers --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('identifiers')"> @click="toggleSection('identifiers')">
<span class="font-semibold">Identifiers</span> <span class="font-semibold">Identifiers</span>
<span x-text="openSections.identifiers ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.identifiers">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.identifiers" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.identifiers" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.identifiers" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ISBN</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ISBN</label>
<input type="text" name="isbn" value={ textToString(book.Isbn) } <input type="text" name="isbn" value={ textToString(book.Isbn) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">ASIN</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">ASIN</label>
<input type="text" name="asin" value={ textToString(book.Asin) } <input type="text" name="asin" value={ textToString(book.Asin) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Goodreads ID</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Goodreads ID</label>
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) } <input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">OpenLibrary ID</label>
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) } <input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Google Books ID</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Google Books ID</label>
<input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) } <input type="text" name="google_books_id" value={ textToString(book.GoogleBooksID) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Web URL</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Web URL</label>
<input type="url" name="web_url" value={ textToString(book.WebUrl) } <input type="url" name="web_url" value={ textToString(book.WebUrl) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Comic/Manga --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('comic')"> @click="toggleSection('comic')">
<span class="font-semibold">Comic/Manga</span> <span class="font-semibold">Comic/Manga</span>
<span x-text="openSections.comic ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.comic">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.comic" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.comic" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.comic" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label for="manga_type" class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Manga Type</label> <label for="manga_type" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Manga Type</label>
<select id="manga_type" name="manga_type" <select id="manga_type" name="manga_type" class="input">
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
<option value="unknown" selected={ textToString(book.MangaType) == "unknown" }>Unknown</option> <option value="unknown" selected={ textToString(book.MangaType) == "unknown" }>Unknown</option>
<option value="no" selected={ textToString(book.MangaType) == "no" }>No</option> <option value="no" selected={ textToString(book.MangaType) == "no" }>No</option>
<option value="yes" selected={ textToString(book.MangaType) == "yes" }>Yes</option> <option value="yes" selected={ textToString(book.MangaType) == "yes" }>Yes</option>
@@ -486,10 +454,8 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</select> </select>
</div> </div>
<div> <div>
<label for="reading_direction" class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Reading Direction</label> <label for="reading_direction" class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Reading Direction</label>
<select id="reading_direction" name="reading_direction" <select id="reading_direction" name="reading_direction" class="input">
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);">
<option value="auto" selected={ textToString(book.ReadingDirection) == "auto" }>Auto</option> <option value="auto" selected={ textToString(book.ReadingDirection) == "auto" }>Auto</option>
<option value="ltr" selected={ textToString(book.ReadingDirection) == "ltr" }>Left to Right</option> <option value="ltr" selected={ textToString(book.ReadingDirection) == "ltr" }>Left to Right</option>
<option value="rtl" selected={ textToString(book.ReadingDirection) == "rtl" }>Right to Left</option> <option value="rtl" selected={ textToString(book.ReadingDirection) == "rtl" }>Right to Left</option>
@@ -497,34 +463,24 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Age Rating</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Age Rating</label>
<input type="text" name="age_rating" value={ textToString(book.AgeRating) } <input type="text" name="age_rating" value={ textToString(book.AgeRating) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Story Arc</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Story Arc</label>
<input type="text" name="story_arc" value={ textToString(book.StoryArc) } <input type="text" name="story_arc" value={ textToString(book.StoryArc) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Imprint</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Imprint</label>
<input type="text" name="imprint" value={ textToString(book.Imprint) } <input type="text" name="imprint" value={ textToString(book.Imprint) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Scan Information</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Scan Information</label>
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) } <input type="text" name="scan_information" value={ textToString(book.ScanInformation) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Metadata Notes</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Metadata Notes</label>
<textarea name="metadata_notes" rows="2" <textarea name="metadata_notes" rows="2" class="input"
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);"
>{ textToString(book.MetadataNotes) }</textarea> >{ textToString(book.MetadataNotes) }</textarea>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -535,40 +491,37 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div> </div>
</div> </div>
</div> </div>
<!-- Technical --> <div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<div class="border rounded-lg" style="border-color: var(--border);"> <button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" style="color: var(--text-primary);"
style="color: var(--text-primary); background-color: var(--bg-primary);"
@click="toggleSection('technical')"> @click="toggleSection('technical')">
<span class="font-semibold">Technical</span> <span class="font-semibold">Technical</span>
<span x-text="openSections.technical ? '▾' : '▸'"></span> <span class="inline-flex">
<span x-show="openSections.technical">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.technical" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button> </button>
<div x-show="openSections.technical" x-transition class="p-4 space-y-3" style="background-color: var(--bg-primary);"> <div x-show="openSections.technical" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Page Count</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Page Count</label>
<input type="number" name="page_count" <input type="number" name="page_count"
value={ fmt.Sprintf("%d", book.PageCount.Int32) } value={ fmt.Sprintf("%d", book.PageCount.Int32) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Contributors (comma-separated)</label>
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) } <input type="text" name="contributors" value={ stringSliceToString(book.Contributors) } class="input" />
class="w-full px-3 py-2 rounded-lg border text-sm"
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">Format</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Format</label>
<input type="text" value={ textToString(book.MimeType) } readonly <input type="text" value={ textToString(book.MimeType) } readonly
class="w-full px-3 py-2 rounded-lg border text-sm opacity-60" class="input opacity-60" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary);">File Size</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">File Size</label>
<input type="text" value={ fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024) } readonly <input type="text" value={ fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024) } readonly
class="w-full px-3 py-2 rounded-lg border text-sm opacity-60" class="input opacity-60" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
@@ -578,16 +531,15 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
<div class="flex justify-end space-x-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);"> <div class="flex justify-end space-x-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);">
<button <button
@click="hideMetadataEditor()" @click="hideMetadataEditor()"
class="px-4 py-2 rounded-lg" class="btn btn-secondary"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
> >
Cancel Cancel
</button> </button>
<button <button
@click="saveMetadata()" @click="saveMetadata()"
class="px-6 py-2 rounded-lg font-semibold" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
@Icon("save", "h-4 w-4")
Save Save
</button> </button>
</div> </div>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+13 -12
View File
@@ -16,20 +16,21 @@ templ BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
</head> </head>
<body class="theme-{ user.Theme }"> <body class="theme-{ user.Theme }">
@Header(user, backUrl) @Header(user, backUrl)
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);"> <div class="app-subbar border-b" style="background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);">
<div class="w-full px-4 py-3 flex items-center gap-4"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-3">
<a href={ backUrl } class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;"> <a href={ backUrl } class="btn btn-ghost">
{ backLabel } @Icon("arrow-left", "h-4 w-4")
<span>{ backLabel }</span>
</a> </a>
</div> </div>
</div> </div>
<div class="w-full px-4 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center gap-4 mb-6"> <div class="flex flex-wrap items-center gap-3 mb-6">
<span class="px-3 py-1 rounded-full text-sm font-semibold" style="background-color: var(--accent); color: white;"> <span class="chip" style="background-color: var(--accent); color: var(--bg-primary);">
{ badgeIcon } { badgeLabel } { badgeIcon } { badgeLabel }
</span> </span>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ title }</h1> <h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">{ title }</h1>
<span class="text-sm" style="color: var(--text-secondary)">{ fmt.Sprintf("%d", len(books)) } books</span> <span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ fmt.Sprintf("%d", len(books)) } books</span>
</div> </div>
if len(books) > 0 { if len(books) > 0 {
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
@@ -38,10 +39,10 @@ templ BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
} }
</div> </div>
} else { } else {
<div class="text-center py-16" style="color: var(--text-secondary)"> <div class="card text-center py-16 px-6">
<div class="text-6xl mb-4">{ emptyIcon }</div> <div class="text-5xl mb-4">{ emptyIcon }</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Books Found</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Books Found</h3>
<p>{ emptyMessage }</p> <p style="color: var(--text-secondary)">{ emptyMessage }</p>
</div> </div>
} }
</div> </div>
+29 -21
View File
@@ -55,7 +55,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b\" style=\"background-color: var(--bg-primary);\"><div class=\"w-full px-4 py-3 flex items-center gap-4\"><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"app-subbar border-b\" style=\"background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);\"><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-3\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -68,77 +68,85 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"text-sm hover:opacity-80 transition-opacity\" style=\"color: var(--text-secondary); text-decoration: none;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"btn btn-ghost\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(backLabel) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(backLabel)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 22, Col: 21} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 23, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a></div></div><div class=\"w-full px-4 py-8\"><div class=\"flex items-center gap-4 mb-6\"><span class=\"px-3 py-1 rounded-full text-sm font-semibold\" style=\"background-color: var(--accent); color: white;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></a></div></div><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"flex flex-wrap items-center gap-3 mb-6\"><span class=\"chip\" style=\"background-color: var(--accent); color: var(--bg-primary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(badgeIcon) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(badgeIcon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 29, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 30, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(badgeLabel) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(badgeLabel)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 29, Col: 32} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 30, Col: 32}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 78} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 32, Col: 93}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h1><span class=\"text-sm\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h1><span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(books))) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(books)))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 32, Col: 95} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 33, Col: 125}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " books</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " books</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(books) > 0 { if len(books) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -148,43 +156,43 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card text-center py-16 px-6\"><div class=\"text-5xl mb-4\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emptyIcon) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emptyIcon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 42, Col: 44} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 43, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Found</h3><p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Books Found</h3><p style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emptyMessage) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emptyMessage)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 44, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 45, Col: 60}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -192,7 +200,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+49 -58
View File
@@ -1,15 +1,22 @@
package templates package templates
templ CollectionModal(collection CollectionData) { templ CollectionModal(collection CollectionData) {
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70"> <div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
if collection.ID != "" { <div class="flex items-center gap-3">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Collection</h2> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
} else { @Icon("folder", "h-5 w-5")
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2> </span>
} if collection.ID != "" {
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Collection</h2>
} else {
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2>
}
</div>
<button type="button" @click="closeCollectionModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
if collection.ID != "" { if collection.ID != "" {
<form <form
@@ -18,65 +25,57 @@ templ CollectionModal(collection CollectionData) {
> >
<input type="hidden" name="id" value={ collection.ID }/> <input type="hidden" name="id" value={ collection.ID }/>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Name</label>
<input <input
type="text" type="text"
name="name" name="name"
value={ collection.Name } value={ collection.Name }
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="My Reading List" placeholder="My Reading List"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
<textarea <textarea
name="description" name="description"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="Optional description" placeholder="Optional description"
rows="3" rows="3"
>{ collection.Description }</textarea> >{ collection.Description }</textarea>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Icon</label>
<!-- Search/Text Input -->
<input <input
type="text" type="text"
id="icon-search" id="icon-search"
class="w-full px-4 py-2 border rounded-lg mb-2" class="input mb-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" placeholder="Search or type emoji..."
placeholder="🔍 Search or type emoji..."
maxlength="4" maxlength="4"
oninput="filterIcons(this.value)" oninput="filterIcons(this.value)"
onfocus="showAllIcons()" onfocus="showAllIcons()"
/> />
<!-- Hidden input for form submission -->
<input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/> <input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/>
<!-- Emoji Grid -->
<div <div
id="icon-grid" id="icon-grid"
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg" class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border"
style="background-color: var(--bg-primary); border-color: var(--border);" style="background-color: var(--bg-primary); border-color: var(--border);"
> ></div>
<!-- Populated dynamically by JavaScript -->
</div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Color</label>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500"></button> <button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400" aria-label="Blue"></button>
<button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500"></button> <button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400" aria-label="Red"></button>
<button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500"></button> <button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400" aria-label="Yellow"></button>
<button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500"></button> <button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400" aria-label="Green"></button>
<button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500"></button> <button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400" aria-label="Purple"></button>
</div> </div>
<input type="hidden" name="color" id="collection-color" value={ collection.Color }/> <input type="hidden" name="color" id="collection-color" value={ collection.Color }/>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Update Collection</button> <button type="submit" class="btn btn-primary">Update Collection</button>
</div> </div>
</form> </form>
} else { } else {
@@ -85,64 +84,56 @@ templ CollectionModal(collection CollectionData) {
hx-redirect="/collections" hx-redirect="/collections"
> >
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Name</label>
<input <input
type="text" type="text"
name="name" name="name"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="My Reading List" placeholder="My Reading List"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
<textarea <textarea
name="description" name="description"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="Optional description" placeholder="Optional description"
rows="3" rows="3"
></textarea> ></textarea>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Icon</label>
<!-- Search/Text Input -->
<input <input
type="text" type="text"
id="icon-search" id="icon-search"
class="w-full px-4 py-2 border rounded-lg mb-2" class="input mb-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" placeholder="Search or type emoji..."
placeholder="🔍 Search or type emoji..."
maxlength="4" maxlength="4"
oninput="filterIcons(this.value)" oninput="filterIcons(this.value)"
onfocus="showAllIcons()" onfocus="showAllIcons()"
/> />
<!-- Hidden input for form submission -->
<input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/> <input type="hidden" name="icon" id="collection-icon" value={ collection.Icon }/>
<!-- Emoji Grid -->
<div <div
id="icon-grid" id="icon-grid"
class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg" class="grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border"
style="background-color: var(--bg-primary); border-color: var(--border);" style="background-color: var(--bg-primary); border-color: var(--border);"
> ></div>
<!-- Populated dynamically by JavaScript -->
</div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Color</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Color</label>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500"></button> <button type="button" @click="selectColor('blue')" class="w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400" aria-label="Blue"></button>
<button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500"></button> <button type="button" @click="selectColor('red')" class="w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400" aria-label="Red"></button>
<button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500"></button> <button type="button" @click="selectColor('yellow')" class="w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400" aria-label="Yellow"></button>
<button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500"></button> <button type="button" @click="selectColor('green')" class="w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400" aria-label="Green"></button>
<button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500"></button> <button type="button" @click="selectColor('purple')" class="w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400" aria-label="Purple"></button>
</div> </div>
<input type="hidden" name="color" id="collection-color" value="blue"/> <input type="hidden" name="color" id="collection-color" value="blue"/>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Create Collection</button> <button type="submit" class="btn btn-primary">Create Collection</button>
</div> </div>
</form> </form>
} }
+37 -21
View File
@@ -29,128 +29,144 @@ func CollectionModal(collection CollectionData) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/70\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"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\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("folder", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if collection.ID != "" { if collection.ID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Collection</h2>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Collection</h2>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Collection</h2>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Collection</h2>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button type=\"button\" @click=\"closeCollectionModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if collection.ID != "" { if collection.ID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<form hx-put=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form hx-put=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + collection.ID) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + collection.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 16, Col: 49} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 23, Col: 49}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" hx-redirect=\"/collections\"><input type=\"hidden\" name=\"id\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-redirect=\"/collections\"><input type=\"hidden\" name=\"id\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 19, Col: 57} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 26, Col: 57}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Name) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 25, Col: 30} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 32, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Optional description\" rows=\"3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" required class=\"input\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"input\" placeholder=\"Optional description\" rows=\"3\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 40, Col: 31} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 45, Col: 31}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"w-full px-4 py-2 border rounded-lg mb-2\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"🔍 Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"><!-- Hidden input for form submission --><input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</textarea></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Icon</label> <input type=\"text\" id=\"icon-search\" class=\"input mb-2\" placeholder=\"Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"> <input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 56, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 58, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400\" aria-label=\"Blue\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400\" aria-label=\"Red\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400\" aria-label=\"Yellow\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400\" aria-label=\"Green\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400\" aria-label=\"Purple\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Color) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Color)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 75, Col: 86} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 74, Col: 86}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Update Collection</button></div></form>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Update Collection</button></div></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<form hx-post=\"/api/collections\" hx-redirect=\"/collections\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Optional description\" rows=\"3\"></textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"w-full px-4 py-2 border rounded-lg mb-2\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"🔍 Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"><!-- Hidden input for form submission --><input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<form hx-post=\"/api/collections\" hx-redirect=\"/collections\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" name=\"name\" required class=\"input\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" class=\"input\" placeholder=\"Optional description\" rows=\"3\"></textarea></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Icon</label> <input type=\"text\" id=\"icon-search\" class=\"input mb-2\" placeholder=\"Search or type emoji...\" maxlength=\"4\" oninput=\"filterIcons(this.value)\" onfocus=\"showAllIcons()\"> <input type=\"hidden\" name=\"icon\" id=\"collection-icon\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 122, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 116, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 border rounded-lg\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"blue\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Collection</button></div></form>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" @click=\"selectColor('blue')\" class=\"w-8 h-8 rounded-full color-option bg-blue-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-blue-400\" aria-label=\"Blue\"></button> <button type=\"button\" @click=\"selectColor('red')\" class=\"w-8 h-8 rounded-full color-option bg-red-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-red-400\" aria-label=\"Red\"></button> <button type=\"button\" @click=\"selectColor('yellow')\" class=\"w-8 h-8 rounded-full color-option bg-yellow-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-yellow-400\" aria-label=\"Yellow\"></button> <button type=\"button\" @click=\"selectColor('green')\" class=\"w-8 h-8 rounded-full color-option bg-green-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-green-400\" aria-label=\"Green\"></button> <button type=\"button\" @click=\"selectColor('purple')\" class=\"w-8 h-8 rounded-full color-option bg-purple-500 hover:ring-2 hover:ring-offset-2 hover:ring-offset-transparent hover:ring-purple-400\" aria-label=\"Purple\"></button></div><input type=\"hidden\" name=\"color\" id=\"collection-color\" value=\"blue\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Create Collection</button></div></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+39 -37
View File
@@ -14,11 +14,12 @@ templ CollectionRules(user User, collection CollectionData) {
@Header(user, "/collections") @Header(user, "/collections")
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6"> <div class="mb-6">
<button @click="backToCollection()" class="btn-secondary px-4 py-2 rounded-lg mb-4"> <button @click="backToCollection()" class="btn btn-secondary mb-4">
Back to Collection @Icon("arrow-left", "h-4 w-4")
Back to Collection
</button> </button>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div> <span class="grid place-items-center h-12 w-12 rounded-2xl shrink-0 text-2xl" style="background-color: var(--accent-muted); color: { collection.Color }">{ collection.Icon }</span>
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1> <h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
<p style="color: var(--text-secondary)">Auto-Assign Rules</p> <p style="color: var(--text-secondary)">Auto-Assign Rules</p>
@@ -28,8 +29,10 @@ templ CollectionRules(user User, collection CollectionData) {
<div class="mb-8"> <div class="mb-8">
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Existing Rules</h2> <h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Existing Rules</h2>
<div id="rules-container" class="space-y-4"> <div id="rules-container" class="space-y-4">
<div id="no-rules" class="card p-6 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div id="no-rules" class="card p-8 text-center">
<div class="text-4xl mb-2">📋</div> <span class="inline-grid place-items-center h-12 w-12 rounded-xl mb-3" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("filter", "h-6 w-6")
</span>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3> <h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3>
<p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p> <p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p>
</div> </div>
@@ -37,16 +40,15 @@ templ CollectionRules(user User, collection CollectionData) {
</div> </div>
<div class="mb-8"> <div class="mb-8">
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Create New Rule</h2> <h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Create New Rule</h2>
<form id="rule-form" @submit="handleCreateRule($event)"> <form id="rule-form" @submit="handleCreateRule($event)" class="card p-6 space-y-6">
<input type="hidden" id="collection-id" value="{ collection.ID }"/> <input type="hidden" id="collection-id" value="{ collection.ID }"/>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Field</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Field</label>
<select <select
id="rule-field" id="rule-field"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="">Select field...</option> <option value="">Select field...</option>
<option value="genre">Genre</option> <option value="genre">Genre</option>
@@ -59,12 +61,11 @@ templ CollectionRules(user User, collection CollectionData) {
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Operator</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Operator</label>
<select <select
id="rule-operator" id="rule-operator"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="">Select operator...</option> <option value="">Select operator...</option>
<option value="equals">equals</option> <option value="equals">equals</option>
@@ -78,18 +79,17 @@ templ CollectionRules(user User, collection CollectionData) {
</select> </select>
</div> </div>
</div> </div>
<div class="mb-6"> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Value</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Value</label>
<input <input
type="text" type="text"
id="rule-value" id="rule-value"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="e.g., Science Fiction" placeholder="e.g., Science Fiction"
/> />
</div> </div>
<div class="mb-6"> <div>
<label class="flex items-center space-x-3 cursor-pointer"> <label class="flex items-center space-x-3 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
@@ -97,16 +97,16 @@ templ CollectionRules(user User, collection CollectionData) {
checked checked
class="w-5 h-5 rounded" class="w-5 h-5 rounded"
/> />
<span style="color: var(--text-primary)">Enable Rule</span> <span class="text-sm font-medium" style="color: var(--text-primary)">Enable Rule</span>
</label> </label>
<p class="text-xs mt-1" style="color: var(--text-secondary)">Uncheck to disable without deleting</p> <p class="text-xs mt-1" style="color: var(--text-secondary)">Uncheck to disable without deleting</p>
</div> </div>
<div class="mb-6"> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Priority</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Priority</label>
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="priority" value="1" class="w-4 h-4"/> <input type="radio" name="priority" value="1" class="w-4 h-4"/>
<span style="color: var(--text-primary)">High</span> <span class="text-sm" style="color: var(--text-primary)">High</span>
</label> </label>
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<input <input
@@ -116,7 +116,7 @@ templ CollectionRules(user User, collection CollectionData) {
checked checked
class="w-4 h-4" class="w-4 h-4"
/> />
<span style="color: var(--text-primary)">Medium</span> <span class="text-sm" style="color: var(--text-primary)">Medium</span>
</label> </label>
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<input <input
@@ -125,36 +125,38 @@ templ CollectionRules(user User, collection CollectionData) {
value="3" value="3"
class="w-4 h-4" class="w-4 h-4"
/> />
<span style="color: var(--text-primary)">Low</span> <span class="text-sm" style="color: var(--text-primary)">Low</span>
</label> </label>
</div> </div>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="testRule()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="testRule()" class="btn btn-secondary">
🧪 Test Rule @Icon("search", "h-4 w-4")
Test Rule
</button> </button>
<button type="button" @click="clearForm()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="clearForm()" class="btn btn-ghost">
Clear Clear
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg"> <button type="submit" class="btn btn-primary">
Add Rule @Icon("plus", "h-4 w-4")
Add Rule
</button> </button>
</div> </div>
</form> </form>
<div id="test-results" class="hidden card p-6 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);"> <div id="test-results" class="hidden card p-6 mt-4 mb-6">
<h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3> <h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3>
<p class="text-sm mb-2" style="color: var(--text-secondary)">Books that would be added by this rule:</p> <p class="text-sm mb-2" style="color: var(--text-secondary)">Books that would be added by this rule:</p>
<div id="test-results-list" class="max-h-64 overflow-y-auto space-y-2"></div> <div id="test-results-list" class="max-h-64 overflow-y-auto space-y-2"></div>
</div> </div>
</div> </div>
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);"> <div class="card p-6">
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary)">Rule Examples</h2> <h2 class="text-xl font-bold mb-4" style="color: var(--text-primary)">Rule Examples</h2>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">1</div> <span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">1</span>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p> <p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p>
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;"> <code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
Field: genre Field: genre
<br/> <br/>
Operator: equals Operator: equals
@@ -164,10 +166,10 @@ templ CollectionRules(user User, collection CollectionData) {
</div> </div>
</div> </div>
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">2</div> <span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">2</span>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p> <p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p>
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;"> <code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
Field: series Field: series
<br/> <br/>
Operator: starts with Operator: starts with
@@ -177,10 +179,10 @@ templ CollectionRules(user User, collection CollectionData) {
</div> </div>
</div> </div>
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">3</div> <span class="grid place-items-center flex-shrink-0 w-8 h-8 rounded-full text-sm font-bold" style="background-color: var(--accent-muted); color: var(--accent);">3</span>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p> <p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p>
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;"> <code class="block mt-1 text-sm rounded-lg" style="color: var(--text-secondary); background-color: var(--bg-primary); padding: 8px 12px;">
Field: copyright_year Field: copyright_year
<br/> <br/>
Operator: greater than Operator: greater than
File diff suppressed because one or more lines are too long
+81 -81
View File
@@ -14,82 +14,89 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
</head> </head>
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }"> <body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
@Header(user, "/collections") @Header(user, "/collections")
<!-- Modal Container -->
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- Actual container page --> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mb-8 flex flex-wrap justify-between items-center gap-4">
<div class="mb-8 flex justify-between items-center"> <div class="flex items-center gap-3">
<div> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">My Collections</h1> @Icon("folder", "h-5 w-5")
<p style="color: var(--text-secondary)">Organize your books into custom collections</p> </span>
<div>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">My Collections</h1>
<p class="text-sm" style="color: var(--text-secondary)">Organize your books into custom collections</p>
</div>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-2">
<button <button
hx-get="/collections/restore-modal" hx-get="/collections/restore-modal"
hx-target="#modal-container" hx-target="#modal-container"
hx-swap="innerHTML" hx-swap="innerHTML"
class="btn-secondary px-4 py-2 rounded-lg" class="btn btn-secondary"
> >
🔄 Restore System @Icon("refresh", "h-4 w-4")
<span>Restore System</span>
</button> </button>
<button <button
hx-get="/collections/create-modal" hx-get="/collections/create-modal"
hx-target="#modal-container" hx-target="#modal-container"
hx-swap="innerHTML" hx-swap="innerHTML"
class="btn-primary px-4 py-2 rounded-lg" class="btn btn-primary"
> >
New Collection @Icon("plus", "h-4 w-4")
<span>New Collection</span>
</button> </button>
</div> </div>
</div> </div>
<div id="collections-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="collections-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
if len(collections) == 0 { if len(collections) == 0 {
<div class="text-center py-16 col-span-full" style="color: var(--text-secondary)"> <div class="card text-center py-16 px-6 col-span-full">
<div class="text-6xl mb-4">📚</div> <div class="grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("folder", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Collections Yet</h3>
<p class="mb-4">Create collections to organize your books</p> <p class="mb-5" style="color: var(--text-secondary)">Create collections to organize your books</p>
<button <button
hx-get="/collections/create-modal" hx-get="/collections/create-modal"
hx-target="#modal-container" hx-target="#modal-container"
hx-swap="innerHTML" hx-swap="innerHTML"
class="btn-primary px-4 py-2 rounded-lg" class="btn btn-primary"
> >
Create Your First Collection @Icon("plus", "h-4 w-4")
<span>Create Your First Collection</span>
</button> </button>
</div> </div>
} }
for _, col := range collections { for _, col := range collections {
<div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block"> <div @click="navigateToCollection($el)" data-href={ "/collections/" + col.ID } class="block">
<div <div
class="card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow" class="card p-6 rounded-2xl border-l-4 cursor-pointer"
style="background-color: var(--bg-secondary);"
data-color={ col.Color } data-color={ col.Color }
> >
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4">
<div class="text-3xl">{ col.Icon }</div> <div class="text-3xl">{ col.Icon }</div>
<div class="flex space-x-2"> <div class="flex gap-1">
<button <button
hx-get={ "/collections/" + col.ID + "/edit-modal" } hx-get={ "/collections/" + col.ID + "/edit-modal" }
hx-target="#modal-container" hx-target="#modal-container"
hx-swap="innerHTML" hx-swap="innerHTML"
class="p-2 hover:opacity-80 rounded" class="icon-btn"
style="color: var(--text-secondary); background-color: var(--bg-primary);" aria-label="Edit collection"
> >
✏️ @Icon("edit", "h-4 w-4")
</button> </button>
<button <button
hx-delete={ "/api/collections/" + col.ID + "" } hx-delete={ "/api/collections/" + col.ID + "" }
hx-redirect="/collections" hx-redirect="/collections"
hx-confirm="Are you sure you want to delete this collection?" hx-confirm="Are you sure you want to delete this collection?"
class="p-2 hover:opacity-80 rounded" class="icon-btn"
style="color: var(--text-secondary); background-color: var(--bg-primary);" aria-label="Delete collection"
> >
🗑️ @Icon("trash", "h-4 w-4")
</button> </button>
</div> </div>
</div> </div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3> <h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ col.Name }</h3>
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ col.Description }</p> <p class="text-sm" style="color: var(--text-secondary)">{ col.Description }</p>
</div> </div>
</div> </div>
} }
@@ -113,62 +120,61 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }"> <body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
@Header(user, "/collections") @Header(user, "/collections")
@LibrarySwitcher(libData, libraryID) @LibrarySwitcher(libData, libraryID)
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6"> <div class="mb-6">
<a href="/collections" class="btn-secondary px-4 py-2 rounded-lg mb-4 inline-block"> <a href="/collections" class="btn btn-secondary mb-4">
Back to Collections @Icon("arrow-left", "h-4 w-4")
<span>Back to Collections</span>
</a> </a>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div> <div class="grid place-items-center h-14 w-14 rounded-2xl text-3xl" style="background-color: var(--accent-muted); color: { collection.Color }">{ collection.Icon }</div>
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1> <h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">{ collection.Name }</h1>
<p style="color: var(--text-secondary)">{ collection.Description }</p> <p class="text-sm" style="color: var(--text-secondary)">{ collection.Description }</p>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-3">
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Books in this Collection</h2> <h2 class="text-lg font-bold tracking-tight" style="color: var(--text-primary)">Books in this Collection</h2>
<span id="selected-count" class="hidden px-3 py-1 text-sm rounded" style="background-color: var(--accent); color: var(--bg-primary);"> <span id="selected-count" class="hidden badge" style="background-color: var(--accent); color: var(--bg-primary);">
0 selected 0 selected
</span> </span>
</div> </div>
<div class="flex gap-3"> <div class="flex flex-wrap gap-2 items-center">
<div class="flex-1 max-w-md"> <div class="flex-1 min-w-[200px] max-w-md">
<input <input
type="text" type="text"
id="collection-search" id="collection-search"
placeholder="Search within collection..." placeholder="Search within collection..."
onkeyup="filterCollectionBooks()" onkeyup="filterCollectionBooks()"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<button <button
id="bulk-remove-btn" id="bulk-remove-btn"
disabled disabled
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" class="btn btn-danger"
> >
🗑️ Remove Selected @Icon("trash", "h-4 w-4")
<span>Remove Selected</span>
</button> </button>
<button <button
@click="$store.bookPicker.open()" @click="$store.bookPicker.open()"
class="btn-primary px-4 py-2 rounded-lg" class="btn btn-primary"
> >
Add Books @Icon("plus", "h-4 w-4")
<span>Add Books</span>
</button> </button>
</div> </div>
</div> </div>
<div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div id="books-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
if len(books) == 0 { if len(books) == 0 {
<div id="empty-state" class="col-span-full text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div> <div id="empty-state" class="col-span-full card text-center py-16" style="color: var(--text-secondary)">No books in this collection yet.</div>
} }
for _, book := range books { for _, book := range books {
<a href={ "/media/" + book.MediaItemID }> <a href={ "/media/" + book.MediaItemID }>
<div <div class="card p-4 rounded-2xl">
class="card p-4 rounded-lg border hover:shadow-lg transition-shadow"
style="background-color: var(--bg-secondary); border-color: var(--border);"
>
<div class="flex gap-4"> <div class="flex gap-4">
<div class="flex-shrink-0 pt-1"> <div class="flex-shrink-0 pt-1">
<input <input
@@ -198,14 +204,14 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
<img <img
src={ book.CoverImagePath } src={ book.CoverImagePath }
alt="Cover" alt="Cover"
class="w-full aspect-[3/4] object-cover rounded shadow-md" class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
onerror="this.src='/static/placeholder-book.svg'" onerror="this.src='/static/placeholder-book.svg'"
/> />
} else { } else {
<img <img
src="/static/placeholder-book.svg" src="/static/placeholder-book.svg"
alt="Cover" alt="Cover"
class="w-full aspect-[3/4] object-cover rounded shadow-md" class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
/> />
} }
</div> </div>
@@ -213,10 +219,10 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);"> <div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
<button <button
@click="removeBook('{ book.MediaItemID }')" @click="removeBook('{ book.MediaItemID }')"
class="px-3 py-1 text-sm border rounded hover:opacity-80" class="btn btn-secondary w-full"
style="border-color: var(--border); color: var(--text-secondary);"
> >
🗑️ Remove from Collection @Icon("trash", "h-4 w-4")
<span>Remove from Collection</span>
</button> </button>
</div> </div>
</div> </div>
@@ -224,7 +230,6 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
} }
</div> </div>
</div> </div>
<!-- Book Picker Modal -->
<div <div
x-data="bookPicker" x-data="bookPicker"
@keyup.escape.window="$store.bookPicker.close()" @keyup.escape.window="$store.bookPicker.close()"
@@ -240,8 +245,8 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
x-transition:leave="transition ease-in duration-150" x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95" x-transition:leave-end="opacity-0 scale-95"
class="card rounded-lg w-full max-w-6xl mx-4 my-8" class="card rounded-2xl w-full max-w-6xl mx-4 my-8"
style="background-color: var(--bg-secondary); border-color: var(--border); display: none;" style="display: none; box-shadow: var(--shadow-pop);"
> >
<div <div
class="flex justify-between items-center p-6 border-b" class="flex justify-between items-center p-6 border-b"
@@ -252,22 +257,23 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
</h2> </h2>
<button <button
@click="$store.bookPicker.close()" @click="$store.bookPicker.close()"
class="p-2 hover:opacity-80 rounded" class="icon-btn"
style="color: var(--text-primary)" aria-label="Close"
></button> >
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<div <div
class="p-4 border-b" class="p-4 border-b"
style="border-color: var(--border);" style="border-color: var(--border);"
> >
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-3 items-center">
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<input <input
type="text" type="text"
name="search" name="search"
placeholder="Search books..." placeholder="Search books..."
class="w-full px-3 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }" hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
hx-target="#book-picker-grid" hx-target="#book-picker-grid"
hx-trigger="keyup changed delay:300ms" hx-trigger="keyup changed delay:300ms"
@@ -279,8 +285,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
type="text" type="text"
name="author_filter" name="author_filter"
placeholder="Author" placeholder="Author"
class="w-full px-3 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }" hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
hx-target="#book-picker-grid" hx-target="#book-picker-grid"
hx-trigger="change" hx-trigger="change"
@@ -292,23 +297,20 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
type="text" type="text"
name="genre_filter" name="genre_filter"
placeholder="Genre" placeholder="Genre"
class="w-full px-3 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }" hx-get="/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }"
hx-target="#book-picker-grid" hx-target="#book-picker-grid"
hx-trigger="change" hx-trigger="change"
hx-include="#book-picker-filters" hx-include="#book-picker-filters"
/> />
</div> </div>
<div> <button
<button @click="$store.bookPicker.clearFilters()"
@click="$store.bookPicker.clearFilters()" class="btn btn-ghost"
class="px-4 py-2 rounded-lg border" >
style="border-color: var(--border); color: var(--text-primary);" @Icon("close", "h-4 w-4")
> <span>Clear</span>
Clear </button>
</button>
</div>
</div> </div>
<form id="filter-form" class="hidden"> <form id="filter-form" class="hidden">
<input type="hidden" name="limit" value="50"/> <input type="hidden" name="limit" value="50"/>
@@ -330,15 +332,13 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@click="$store.bookPicker.close()" @click="$store.bookPicker.close()"
class="px-4 py-2 rounded-lg border" class="btn btn-secondary"
style="border-color: var(--border); color: var(--text-primary);"
> >
Cancel Cancel
</button> </button>
<button <button
@click="$store.bookPicker.submit()" @click="$store.bookPicker.submit()"
class="px-4 py-2 rounded-lg font-medium" class="btn btn-primary"
style="background-color: var(--accent); color: var(--bg-primary);"
> >
Add Selected Books Add Selected Books
</button> </button>
+151 -47
View File
@@ -39,114 +39,170 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container --><div id=\"modal-container\"></div><!-- Actual container page --><div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">My Collections</h1><p style=\"color: var(--text-secondary)\">Organize your books into custom collections</p></div><div class=\"flex gap-3\"><button hx-get=\"/collections/restore-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-secondary px-4 py-2 rounded-lg\">🔄 Restore System</button> <button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-primary px-4 py-2 rounded-lg\"> New Collection</button></div></div><div id=\"collections-list\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"modal-container\"></div><div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex flex-wrap justify-between items-center gap-4\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("folder", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">My Collections</h1><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Organize your books into custom collections</p></div></div><div class=\"flex gap-2\"><button hx-get=\"/collections/restore-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span>Restore System</span></button> <button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>New Collection</span></button></div></div><div id=\"collections-list\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(collections) == 0 { if len(collections) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"text-center py-16 col-span-full\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Collections Yet</h3><p class=\"mb-4\">Create collections to organize your books</p><button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Your First Collection</button></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card text-center py-16 px-6 col-span-full\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("folder", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Collections Yet</h3><p class=\"mb-5\" style=\"color: var(--text-secondary)\">Create collections to organize your books</p><button hx-get=\"/collections/create-modal\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Create Your First Collection</span></button></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, col := range collections { for _, col := range collections {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div @click=\"navigateToCollection($el)\" data-href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div @click=\"navigateToCollection($el)\" data-href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 62, Col: 82} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 70, Col: 82}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"block\"><div class=\"card p-6 rounded-lg border-l-4 cursor-pointer hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary);\" data-color=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"block\"><div class=\"card p-6 rounded-2xl border-l-4 cursor-pointer\" data-color=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(col.Color) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(col.Color)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 66, Col: 30} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 73, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><div class=\"flex justify-between items-start mb-4\"><div class=\"text-3xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><div class=\"flex justify-between items-start mb-4\"><div class=\"text-3xl\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 69, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 76, Col: 41}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"flex space-x-2\"><button hx-get=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div class=\"flex gap-1\"><button hx-get=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID + "/edit-modal") templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID + "/edit-modal")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 72, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 79, Col: 60}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">✏️</button> <button hx-delete=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"icon-btn\" aria-label=\"Edit collection\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</button> <button hx-delete=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + col.ID + "") templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + col.ID + "")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 81, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 88, Col: 56}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-redirect=\"/collections\" hx-confirm=\"Are you sure you want to delete this collection?\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">🗑️</button></div></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-redirect=\"/collections\" hx-confirm=\"Are you sure you want to delete this collection?\" class=\"icon-btn\" aria-label=\"Delete collection\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button></div></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 91, Col: 92} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 98, Col: 92}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h3><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 92, Col: 86} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 99, Col: 81}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -154,7 +210,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -183,20 +239,20 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
templ_7745c5c3_Var9 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 109, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 116, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -208,139 +264,187 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6\"><a href=\"/collections\" class=\"btn-secondary px-4 py-2 rounded-lg mb-4 inline-block\">← Back to Collections</a><div class=\"flex items-center gap-4\"><div class=\"text-4xl\" style=\"color: { collection.Color }\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6\"><a href=\"/collections\" class=\"btn btn-secondary mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>Back to Collections</span></a><div class=\"flex items-center gap-4\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl text-3xl\" style=\"background-color: var(--accent-muted); color: { collection.Color }\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 122, Col: 81} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 130, Col: 166}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 124, Col: 90} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 132, Col: 105}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h1><p style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</h1><p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 125, Col: 71} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 133, Col: 87}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p></div></div></div><div class=\"mb-6 flex justify-between items-center\"><div class=\"flex items-center gap-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Books in this Collection</h2><span id=\"selected-count\" class=\"hidden px-3 py-1 text-sm rounded\" style=\"background-color: var(--accent); color: var(--bg-primary);\">0 selected</span></div><div class=\"flex gap-3\"><div class=\"flex-1 max-w-md\"><input type=\"text\" id=\"collection-search\" placeholder=\"Search within collection...\" onkeyup=\"filterCollectionBooks()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><button id=\"bulk-remove-btn\" disabled class=\"btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed\">🗑️ Remove Selected</button> <button @click=\"$store.bookPicker.open()\" class=\"btn-primary px-4 py-2 rounded-lg\"> Add Books</button></div></div><div id=\"books-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p></div></div></div><div class=\"mb-6 flex flex-wrap justify-between items-center gap-4\"><div class=\"flex items-center gap-3\"><h2 class=\"text-lg font-bold tracking-tight\" style=\"color: var(--text-primary)\">Books in this Collection</h2><span id=\"selected-count\" class=\"hidden badge\" style=\"background-color: var(--accent); color: var(--bg-primary);\">0 selected</span></div><div class=\"flex flex-wrap gap-2 items-center\"><div class=\"flex-1 min-w-[200px] max-w-md\"><input type=\"text\" id=\"collection-search\" placeholder=\"Search within collection...\" onkeyup=\"filterCollectionBooks()\" class=\"input\"></div><button id=\"bulk-remove-btn\" disabled class=\"btn btn-danger\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span>Remove Selected</span></button> <button @click=\"$store.bookPicker.open()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<span>Add Books</span></button></div></div><div id=\"books-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(books) == 0 { if len(books) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div id=\"empty-state\" class=\"col-span-full text-center py-16\" style=\"color: var(--text-secondary)\">No books in this collection yet.</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div id=\"empty-state\" class=\"col-span-full card text-center py-16\" style=\"color: var(--text-secondary)\">No books in this collection yet.</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, book := range books { for _, book := range books {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 templ.SafeURL var templ_7745c5c3_Var14 templ.SafeURL
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 167, Col: 44} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 176, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\"><div class=\"card p-4 rounded-lg border hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-4\"><div class=\"flex-shrink-0 pt-1\"><input type=\"checkbox\" onchange=\"toggleBookForRemoval('{ book.MediaItemID }')\" class=\"w-5 h-5\"></div><div class=\"flex-1 min-w-0\"><h3 class=\"font-semibold text-lg mb-1 line-clamp-2\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"><div class=\"card p-4 rounded-2xl\"><div class=\"flex gap-4\"><div class=\"flex-shrink-0 pt-1\"><input type=\"checkbox\" onchange=\"toggleBookForRemoval('{ book.MediaItemID }')\" class=\"w-5 h-5\"></div><div class=\"flex-1 min-w-0\"><h3 class=\"font-semibold text-lg mb-1 line-clamp-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 185, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 191, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</h3>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</h3>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if book.Author != "" { if book.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\">by ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\">by ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 192, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 198, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><div class=\"flex-shrink-0 w-16 sm:w-20\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div><div class=\"flex-shrink-0 w-16 sm:w-20\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if book.CoverImagePath != "" { if book.CoverImagePath != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<img src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<img src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 199, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 205, Col: 37}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<img src=\"/static/placeholder-book.svg\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded shadow-md\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<img src=\"/static/placeholder-book.svg\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div><div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><button @click=\"removeBook('{ book.MediaItemID }')\" class=\"px-3 py-1 text-sm border rounded hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-secondary);\">🗑️ Remove from Collection</button></div></div></a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></div><div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><button @click=\"removeBook('{ book.MediaItemID }')\" class=\"btn btn-secondary w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<span>Remove from Collection</span></button></div></div></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></div><!-- Book Picker Modal --><div x-data=\"bookPicker\" @keyup.escape.window=\"$store.bookPicker.close()\" class=\"fixed inset-0 z-50 flex items-center justify-center\" style=\"display: none;\"><div @click.stop x-show=\"$store.bookPicker.isOpen\" 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=\"card rounded-lg w-full max-w-6xl mx-4 my-8\" style=\"background-color: var(--bg-secondary); border-color: var(--border); display: none;\"><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Books to Collection</h2><button @click=\"$store.bookPicker.close()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div class=\"p-4 border-b\" style=\"border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div class=\"flex-1 min-w-[200px]\"><input type=\"text\" name=\"search\" placeholder=\"Search books...\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"keyup changed delay:300ms\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"author_filter\" placeholder=\"Author\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"genre_filter\" placeholder=\"Genre\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div><button @click=\"$store.bookPicker.clearFilters()\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">✕ Clear</button></div></div><form id=\"filter-form\" class=\"hidden\"><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div><div id=\"book-picker-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto\"></div><div class=\"p-4 border-t flex justify-between items-center\" style=\"border-color: var(--border);\"><div class=\"text-sm\" style=\"color: var(--text-secondary);\"><span x-text=\"$store.bookPicker.selectedCount\"></span> books selected</div><div class=\"flex gap-2\"><button @click=\"$store.bookPicker.close()\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">Cancel</button> <button @click=\"$store.bookPicker.submit()\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent); color: var(--bg-primary);\">Add Selected Books</button></div></div></div></div></body><div id=\"collection-data\" data-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div></div><div x-data=\"bookPicker\" @keyup.escape.window=\"$store.bookPicker.close()\" class=\"fixed inset-0 z-50 flex items-center justify-center\" style=\"display: none;\"><div @click.stop x-show=\"$store.bookPicker.isOpen\" 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=\"card rounded-2xl w-full max-w-6xl mx-4 my-8\" style=\"display: none; box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Books to Collection</h2><button @click=\"$store.bookPicker.close()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</button></div><div class=\"p-4 border-b\" style=\"border-color: var(--border);\"><div class=\"flex flex-wrap gap-3 items-center\"><div class=\"flex-1 min-w-[200px]\"><input type=\"text\" name=\"search\" placeholder=\"Search books...\" class=\"input\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"keyup changed delay:300ms\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"author_filter\" placeholder=\"Author\" class=\"input\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><div class=\"flex-1 min-w-[150px]\"><input type=\"text\" name=\"genre_filter\" placeholder=\"Genre\" class=\"input\" hx-get=\"/api/media-items/filtered?show_checkbox=true&collection_id={ collection.ID }\" hx-target=\"#book-picker-grid\" hx-trigger=\"change\" hx-include=\"#book-picker-filters\"></div><button @click=\"$store.bookPicker.clearFilters()\" class=\"btn btn-ghost\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<span>Clear</span></button></div><form id=\"filter-form\" class=\"hidden\"><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div><div id=\"book-picker-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4 max-h-96 overflow-y-auto\"></div><div class=\"p-4 border-t flex justify-between items-center\" style=\"border-color: var(--border);\"><div class=\"text-sm\" style=\"color: var(--text-secondary);\"><span x-text=\"$store.bookPicker.selectedCount\"></span> books selected</div><div class=\"flex gap-2\"><button @click=\"$store.bookPicker.close()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"$store.bookPicker.submit()\" class=\"btn btn-primary\">Add Selected Books</button></div></div></div></div></body><div id=\"collection-data\" data-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -353,7 +457,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" data-library-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" data-library-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -366,7 +470,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" style=\"display: none;\"></div></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" style=\"display: none;\"></div></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+82 -69
View File
@@ -14,55 +14,66 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
</head> </head>
<body x-data="conflicts" class="theme-{ user.Theme }"> <body x-data="conflicts" class="theme-{ user.Theme }">
@Header(user, "/conflicts") @Header(user, "/conflicts")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center gap-4 flex-wrap">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Conflicts</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: color-mix(in srgb, var(--status-warning) 16%, transparent); color: var(--status-warning);">
@Icon("alert", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Sync Conflicts</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p>
</div> </div>
<div class="flex space-x-3"> <div class="flex space-x-3">
<select <select
id="status-filter" id="status-filter"
onchange="filterConflicts()" onchange="filterConflicts()"
class="px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="unresolved">Unresolved Only</option> <option value="unresolved">Unresolved Only</option>
<option value="all">All Conflicts</option> <option value="all">All Conflicts</option>
<option value="resolved">Resolved Only</option> <option value="resolved">Resolved Only</option>
</select> </select>
<button @click="dismissAllResolved()" class="btn-secondary px-4 py-2 rounded-lg"> <button @click="dismissAllResolved()" class="btn btn-secondary">Dismiss All Resolved</button>
Dismiss All Resolved
</button>
</div> </div>
</div> </div>
<div id="stats-bar" class="mt-4 flex space-x-6 text-sm"> <div id="stats-bar" class="mt-4 grid grid-cols-3 gap-4 max-w-lg">
<span style="color: var(--text-secondary)">Total: <strong style="color: var(--text-primary)">{ total }</strong></span> <div class="stat-card text-center py-3">
<span style="color: var(--text-secondary)">Unresolved: <strong style="color: #f59e0b;">{ unresolved }</strong></span> <div class="text-2xl font-bold" style="color: var(--text-primary);">{ total }</div>
<span style="color: var(--text-secondary)">Resolved: <strong style="color: #10b981;">{ total - unresolved }</strong></span> <div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Total</div>
</div>
<div class="stat-card text-center py-3">
<div class="text-2xl font-bold" style="color: var(--status-warning);">{ unresolved }</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Unresolved</div>
</div>
<div class="stat-card text-center py-3">
<div class="text-2xl font-bold" style="color: var(--status-success);">{ total - unresolved }</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Resolved</div>
</div>
</div> </div>
<div id="bulk-actions" class="mt-4 hidden"> <div id="bulk-actions" class="mt-4 hidden">
<div class="flex items-center justify-between p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-4 flex items-center justify-between gap-4 flex-wrap">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="text-sm font-semibold" style="color: var(--text-primary);"> <span class="text-sm font-semibold" style="color: var(--text-primary);">
<span id="selected-count">0</span> conflicts selected <span id="selected-count">0</span> conflicts selected
</span> </span>
<select <select
id="bulk-strategy" id="bulk-strategy"
class="px-3 py-2 border rounded-lg text-sm" class="input w-auto"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="most_recent">Most Recent</option> <option value="most_recent">Most Recent</option>
<option value="highest_progress">Highest Progress</option> <option value="highest_progress">Highest Progress</option>
</select> </select>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<button @click="bulkResolve()" class="btn-primary px-4 py-2 rounded-lg text-sm"> <button @click="bulkResolve()" class="btn btn-primary text-sm">
Resolve Selected @Icon("check", "h-4 w-4")
Resolve Selected
</button> </button>
<button @click="bulkDismiss()" class="btn-danger px-4 py-2 rounded-lg text-sm"> <button @click="bulkDismiss()" class="btn btn-danger text-sm">
Dismiss Selected Dismiss Selected
</button> </button>
</div> </div>
</div> </div>
@@ -72,12 +83,14 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
<div class="loading-spinner mx-auto mb-4"></div> <div class="loading-spinner mx-auto mb-4"></div>
<p>Loading conflicts...</p> <p>Loading conflicts...</p>
</div> </div>
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)"> <div id="empty-state" class="hidden card text-center py-16">
<div class="text-6xl mb-4"></div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
@Icon("check-circle", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3>
<p>Your devices are in sync! No conflicts to resolve.</p> <p style="color: var(--text-secondary)">Your devices are in sync! No conflicts to resolve.</p>
</div> </div>
<div id="conflicts-list" class="space-y-6"> <div id="conflicts-list" class="space-y-4">
<div id="select-all-container" class="mb-4 hidden"> <div id="select-all-container" class="mb-4 hidden">
<label class="flex items-center space-x-2 cursor-pointer"> <label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" id="select-all-conflicts" class="w-5 h-5 rounded" onchange="toggleAllConflicts()"/> <input type="checkbox" id="select-all-conflicts" class="w-5 h-5 rounded" onchange="toggleAllConflicts()"/>
@@ -85,15 +98,17 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
</label> </label>
</div> </div>
if len(conflicts) == 0 { if len(conflicts) == 0 {
<div class="text-center py-16" style="color: var(--text-secondary)"> <div class="card text-center py-16">
<div class="text-6xl mb-4"></div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
@Icon("check-circle", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3>
<p>Your devices are in sync! No conflicts to resolve.</p> <p style="color: var(--text-secondary)">Your devices are in sync! No conflicts to resolve.</p>
</div> </div>
} }
for _, conflict := range conflicts { for _, conflict := range conflicts {
<div class="conflict-item card p-6 rounded-lg border" data-conflict-id="{ conflict.ID }" style="background-color: var(--bg-secondary); border-color: var(--border); border-left-width: 4px; border-left-style: solid; border-left-color: rgb(245, 158, 11);"> <div class="conflict-item card p-6" data-conflict-id="{ conflict.ID }" style="border-left-width: 4px; border-left-color: var(--status-warning);">
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4 gap-4">
<div class="flex items-start space-x-4"> <div class="flex items-start space-x-4">
<input <input
type="checkbox" type="checkbox"
@@ -101,14 +116,17 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
data-conflict-id="{ conflict.ID }" data-conflict-id="{ conflict.ID }"
onchange="updateBulkActions()" onchange="updateBulkActions()"
/> />
<div> <div class="flex items-start gap-3">
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3> <span style="color: var(--status-warning);">
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p> @Icon("alert", "h-5 w-5")
</span>
<div>
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3>
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p>
</div>
</div> </div>
</div> </div>
<button @click="showResolveModal('{ conflict.ID }')" class="btn-primary px-4 py-2 rounded-lg"> <button @click="showResolveModal('{ conflict.ID }')" class="btn btn-primary">Resolve</button>
Resolve
</button>
</div> </div>
<div class="text-sm" style="color: var(--text-secondary)"> <div class="text-sm" style="color: var(--text-secondary)">
Created: { FormatInTimezone(conflict.CreatedAt, user.Timezone) } Created: { FormatInTimezone(conflict.CreatedAt, user.Timezone) }
@@ -119,94 +137,90 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
</div> </div>
} }
</div> </div>
<div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);"> <div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-4xl my-8" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<div> <div>
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Resolve Conflict</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Resolve Conflict</h2>
<p class="text-sm" id="conflict-book-title" style="color: var(--text-secondary);">Book Title</p> <p class="text-sm" id="conflict-book-title" style="color: var(--text-secondary);">Book Title</p>
</div> </div>
<button @click="hideResolveModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);"> <button @click="hideResolveModal()" class="icon-btn" aria-label="Close">
X @Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
<form id="conflict-resolution-form" @submit="handleResolveSubmit($event)"> <form id="conflict-resolution-form" @submit="handleResolveSubmit($event)">
<input type="hidden" id="conflict-id"/> <input type="hidden" id="conflict-id"/>
<div id="conflict-details" class="mb-6"></div> <div id="conflict-details" class="mb-6"></div>
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);"> <div class="card p-4 mb-6">
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Resolution Options</h3> <h3 class="font-semibold mb-4" style="color: var(--text-primary)">Resolution Options</h3>
<div class="space-y-3 mb-6"> <div class="space-y-3 mb-6">
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);"> <label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
<input type="radio" name="winner" value="koreader" class="mr-3" required/> <input type="radio" name="winner" value="koreader" class="w-4 h-4" required/>
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold" style="color: var(--text-primary);">Keep KOReader Progress</span> <span class="font-semibold" style="color: var(--text-primary);">Keep KOReader Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your KOReader device</p> <p class="text-xs" style="color: var(--text-secondary);">Use progress from your KOReader device</p>
</div> </div>
</label> </label>
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);"> <label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
<input type="radio" name="winner" value="kobo" class="mr-3"/> <input type="radio" name="winner" value="kobo" class="w-4 h-4"/>
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold" style="color: var(--text-primary);">Keep Kobo Progress</span> <span class="font-semibold" style="color: var(--text-primary);">Keep Kobo Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your Kobo device</p> <p class="text-xs" style="color: var(--text-secondary);">Use progress from your Kobo device</p>
</div> </div>
</label> </label>
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);"> <label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
<input type="radio" name="winner" value="web" class="mr-3"/> <input type="radio" name="winner" value="web" class="w-4 h-4"/>
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold" style="color: var(--text-primary);">Keep Web Progress</span> <span class="font-semibold" style="color: var(--text-primary);">Keep Web Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from the web interface</p> <p class="text-xs" style="color: var(--text-secondary);">Use progress from the web interface</p>
</div> </div>
</label> </label>
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);"> <label class="flex items-center gap-3 p-3 rounded-lg cursor-pointer card transition-colors" style="border-color: var(--border);">
<input type="radio" name="winner" value="manual" class="mr-3"/> <input type="radio" name="winner" value="manual" class="w-4 h-4"/>
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold" style="color: var(--text-primary);">Manual Override</span> <span class="font-semibold" style="color: var(--text-primary);">Manual Override</span>
<p class="text-xs" style="color: var(--text-secondary);">Specify a custom progress value</p> <p class="text-xs" style="color: var(--text-secondary);">Specify a custom progress value</p>
</div> </div>
</label> </label>
</div> </div>
<div id="manual-override" class="hidden mb-4 p-4 border rounded-lg" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div id="manual-override" class="hidden mb-4 p-4 rounded-lg" style="background-color: var(--bg-primary); border: 1px solid var(--border);">
<h4 class="font-semibold mb-3" style="color: var(--text-primary);">Manual Progress</h4> <h4 class="font-semibold mb-3" style="color: var(--text-primary);">Manual Progress</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Percentage (0-100)</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Percentage (0-100)</label>
<input <input
type="number" type="number"
id="manual-percentage" id="manual-percentage"
min="0" min="0"
max="100" max="100"
step="0.01" step="0.01"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Page Number</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Page Number</label>
<input <input
type="number" type="number"
id="manual-page" id="manual-page"
min="1" min="1"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">EPUB CFI</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">EPUB CFI</label>
<input <input
type="text" type="text"
id="manual-epubcfi" id="manual-epubcfi"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Chapter</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Chapter</label>
<input <input
type="number" type="number"
id="manual-chapter" id="manual-chapter"
min="1" min="1"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
</div> </div>
@@ -218,20 +232,19 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
</label> </label>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Resolution Reason (optional)</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary);">Resolution Reason (optional)</label>
<input <input
type="text" type="text"
id="resolution-reason" id="resolution-reason"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="e.g., Device was offline, using most recent progress" placeholder="e.g., Device was offline, using most recent progress"
/> />
</div> </div>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3 flex-wrap">
<button type="button" @click="hideResolveModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" @click="hideResolveModal()" class="btn btn-secondary">Cancel</button>
<button type="button" @click="deleteConflict()" class="btn-danger px-4 py-2 rounded-lg">Dismiss</button> <button type="button" @click="deleteConflict()" class="btn btn-danger">Dismiss</button>
<button type="submit" class="btn-primary px-6 py-2 rounded-lg">Resolve Conflict</button> <button type="submit" class="btn btn-primary">Resolve Conflict</button>
</div> </div>
</form> </form>
</div> </div>
File diff suppressed because one or more lines are too long
+60 -53
View File
@@ -12,75 +12,82 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
</head> </head>
<body class="theme-{ user.Theme }"> <body class="theme-{ user.Theme }">
@Header(user, "/custom-section") @Header(user, "/custom-section")
<main class="max-w-4xl mx-auto px-4 py-8"> <main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8 max-w-4xl">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Create Custom Section</h1> <div class="mb-8">
<p class="mb-6" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p> <div class="flex items-center gap-3 mb-1">
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("layers", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Create Custom Section</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
</div>
<form id="custom-section-form" class="space-y-6"> <form id="custom-section-form" class="space-y-6">
<!-- Section Details --> <div class="card p-6">
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text-primary)">
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Section Details</h2> @Icon("info", "h-5 w-5")
Section Details
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Name *</label> <label for="section-name" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Name *</label>
<input <input
type="text" type="text"
id="section-name" id="section-name"
name="name" name="name"
required required
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label> <label for="section-icon" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Icon (emoji)</label>
<input <input
type="text" type="text"
id="section-icon" id="section-icon"
name="icon" name="icon"
maxlength="4" maxlength="4"
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="📚" placeholder="📚"
/> />
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Description</label> <label for="section-description" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Description</label>
<textarea <textarea
id="section-description" id="section-description"
name="description" name="description"
rows="2" rows="2"
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
></textarea> ></textarea>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Library *</label> <label for="section-library" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Library *</label>
<select <select
id="section-library" id="section-library"
name="library_id" name="library_id"
required required
class="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="">Select a library...</option> <option value="">Select a library</option>
for _, lib := range libraries { for _, lib := range libraries {
<option value={ lib.ID }>{ lib.Name }</option> <option value={ lib.ID }>{ lib.Name }</option>
} }
</select> </select>
</div> </div>
</div> </div>
<!-- Filter Rules --> <div class="card p-6">
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Filter Rules</h2> <h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
@Icon("filter", "h-5 w-5")
Filter Rules
</h2>
<button <button
type="button" type="button"
id="add-rule-btn" id="add-rule-btn"
class="px-3 py-1 rounded-lg text-sm font-medium" class="btn btn-primary text-sm"
style="background-color: var(--accent);"
> >
+ Add Rule @Icon("plus", "h-4 w-4")
Add Rule
</button> </button>
</div> </div>
<p class="text-sm mb-4" style="color: var(--text-secondary)"> <p class="text-sm mb-4" style="color: var(--text-secondary)">
@@ -88,101 +95,101 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
</p> </p>
<div id="rules-container" class="space-y-3"></div> <div id="rules-container" class="space-y-3"></div>
<div class="mt-4 flex items-center gap-2"> <div class="mt-4 flex items-center gap-2">
<label class="text-sm font-medium" style="color: var(--text-secondary)">Match:</label> <label for="match-type" class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Match:</label>
<select <select
id="match-type" id="match-type"
name="match_type" name="match_type"
class="px-3 py-1 rounded border" class="input w-auto"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="all">ALL rules (AND)</option> <option value="all">ALL rules (AND)</option>
<option value="any">ANY rule (OR)</option> <option value="any">ANY rule (OR)</option>
</select> </select>
</div> </div>
</div> </div>
<!-- Manual Book Selection --> <div class="card p-6">
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <h2 class="text-lg font-semibold mb-4 flex items-center gap-2" style="color: var(--text-primary)">
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Manual Book Selection</h2> @Icon("book", "h-5 w-5")
Manual Book Selection
</h2>
<p class="text-sm mb-4" style="color: var(--text-secondary)"> <p class="text-sm mb-4" style="color: var(--text-secondary)">
Add specific books to this section. Use the search to find and select multiple books. Add specific books to this section. Use the search to find and select multiple books.
</p> </p>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search Books</label> <label for="book-search" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Search Books</label>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="text" type="text"
id="book-search" id="book-search"
name="book_search" name="book_search"
class="flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500" class="input flex-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" placeholder="Search by title or author…"
placeholder="Search by title or author..."
autocomplete="off" autocomplete="off"
/> />
<button <button
type="button" type="button"
id="search-books-btn" id="search-books-btn"
class="px-4 py-2 rounded-lg font-medium" class="btn btn-primary"
style="background-color: var(--accent);"
> >
@Icon("search", "h-4 w-4")
Search Search
</button> </button>
</div> </div>
</div> </div>
<div <div
id="search-results" id="search-results"
class="hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto" class="hidden mb-4 card p-3 max-h-60 overflow-y-auto space-y-2"
style="background-color: var(--bg-primary);" style="background-color: var(--bg-primary);"
></div> ></div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label> <label class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Selected Books</label>
<div <div
id="selected-books" id="selected-books"
class="min-h-[60px] p-3 rounded-lg border-2 border-dashed" class="min-h-[60px] p-3 rounded-xl border-2 border-dashed"
style="border-color: var(--border); background-color: var(--bg-primary);" style="border-color: var(--border-strong); background-color: var(--bg-primary);"
> >
<p class="text-sm text-center" style="color: var(--text-secondary);">No books selected</p> <p class="text-sm text-center" style="color: var(--text-secondary);">No books selected</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Live Preview --> <div class="card p-6">
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold" style="color: var(--text-primary)">Live Preview</h2> <h2 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
@Icon("play", "h-5 w-5")
Live Preview
</h2>
<button <button
type="button" type="button"
id="preview-btn" id="preview-btn"
class="px-4 py-2 rounded-lg font-medium" class="btn btn-secondary text-sm"
style="background-color: var(--accent);"
> >
@Icon("refresh", "h-4 w-4")
Refresh Preview Refresh Preview
</button> </button>
</div> </div>
<div <div
id="preview-container" id="preview-container"
class="p-4 rounded-lg" class="card p-4"
style="background-color: var(--bg-primary); min-height: 200px;" style="background-color: var(--bg-primary); min-height: 200px;"
> >
<p class="text-center" style="color: var(--text-secondary);"> <p class="text-center text-sm" style="color: var(--text-secondary);">
Add filter rules or select books to see a preview of your custom section. Add filter rules or select books to see a preview of your custom section.
</p> </p>
</div> </div>
</div> </div>
<!-- Form Actions -->
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button <button
type="button" type="button"
id="cancel-btn" id="cancel-btn"
class="px-6 py-2 rounded-lg font-medium border hover:opacity-80" class="btn btn-secondary"
style="border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
id="save-section-btn" id="save-section-btn"
class="px-6 py-2 rounded-lg font-medium text-white hover:opacity-90" class="btn btn-primary"
style="background-color: var(--accent);"
> >
@Icon("save", "h-4 w-4")
Save Section Save Section
</button> </button>
</div> </div>
+80 -8
View File
@@ -37,43 +37,115 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"max-w-4xl mx-auto px-4 py-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Create Custom Section</h1><p class=\"mb-6\" style=\"color: var(--text-secondary)\">Build a custom dashboard section by defining filter rules or manually selecting books.</p><form id=\"custom-section-form\" class=\"space-y-6\"><!-- Section Details --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><h2 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Section Details</h2><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name *</label> <input type=\"text\" id=\"section-name\" name=\"name\" required class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon (emoji)</label> <input type=\"text\" id=\"section-icon\" name=\"icon\" maxlength=\"4\" class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"📚\"></div></div><div class=\"mt-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea id=\"section-description\" name=\"description\" rows=\"2\" class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></textarea></div><div class=\"mt-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Library *</label> <select id=\"section-library\" name=\"library_id\" required class=\"w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select a library...</option> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8 max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("layers", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Create Custom Section</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Build a custom dashboard section by defining filter rules or manually selecting books.</p></div><form id=\"custom-section-form\" class=\"space-y-6\"><div class=\"card p-6\"><h2 class=\"text-lg font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("info", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Section Details</h2><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label for=\"section-name\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Name *</label> <input type=\"text\" id=\"section-name\" name=\"name\" required class=\"input\"></div><div><label for=\"section-icon\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Icon (emoji)</label> <input type=\"text\" id=\"section-icon\" name=\"icon\" maxlength=\"4\" class=\"input\" placeholder=\"📚\"></div></div><div class=\"mt-4\"><label for=\"section-description\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea id=\"section-description\" name=\"description\" rows=\"2\" class=\"input\"></textarea></div><div class=\"mt-4\"><label for=\"section-library\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Library *</label> <select id=\"section-library\" name=\"library_id\" required class=\"input\"><option value=\"\">Select a library…</option> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, lib := range libraries { for _, lib := range libraries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<option value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 68, Col: 31} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 73, Col: 31}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 68, Col: 44} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 73, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</option>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</option>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</select></div></div><!-- Filter Rules --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Filter Rules</h2><button type=\"button\" id=\"add-rule-btn\" class=\"px-3 py-1 rounded-lg text-sm font-medium\" style=\"background-color: var(--accent);\">+ Add Rule</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.</p><div id=\"rules-container\" class=\"space-y-3\"></div><div class=\"mt-4 flex items-center gap-2\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Match:</label> <select id=\"match-type\" name=\"match_type\" class=\"px-3 py-1 rounded border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">ALL rules (AND)</option> <option value=\"any\">ANY rule (OR)</option></select></div></div><!-- Manual Book Selection --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><h2 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Manual Book Selection</h2><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Add specific books to this section. Use the search to find and select multiple books.</p><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Search Books</label><div class=\"flex gap-2\"><input type=\"text\" id=\"book-search\" name=\"book_search\" class=\"flex-1 px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Search by title or author...\" autocomplete=\"off\"> <button type=\"button\" id=\"search-books-btn\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent);\">Search</button></div></div><div id=\"search-results\" class=\"hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto\" style=\"background-color: var(--bg-primary);\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Selected Books</label><div id=\"selected-books\" class=\"min-h-[60px] p-3 rounded-lg border-2 border-dashed\" style=\"border-color: var(--border); background-color: var(--bg-primary);\"><p class=\"text-sm text-center\" style=\"color: var(--text-secondary);\">No books selected</p></div></div></div><!-- Live Preview --><div class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Live Preview</h2><button type=\"button\" id=\"preview-btn\" class=\"px-4 py-2 rounded-lg font-medium\" style=\"background-color: var(--accent);\">Refresh Preview</button></div><div id=\"preview-container\" class=\"p-4 rounded-lg\" style=\"background-color: var(--bg-primary); min-height: 200px;\"><p class=\"text-center\" style=\"color: var(--text-secondary);\">Add filter rules or select books to see a preview of your custom section.</p></div></div><!-- Form Actions --><div class=\"flex justify-end gap-3\"><button type=\"button\" id=\"cancel-btn\" class=\"px-6 py-2 rounded-lg font-medium border hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-primary); background-color: var(--bg-secondary);\">Cancel</button> <button type=\"submit\" id=\"save-section-btn\" class=\"px-6 py-2 rounded-lg font-medium text-white hover:opacity-90\" style=\"background-color: var(--accent);\">Save Section</button></div></form></main>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</select></div></div><div class=\"card p-6\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("filter", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Filter Rules</h2><button type=\"button\" id=\"add-rule-btn\" class=\"btn btn-primary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("plus", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Add Rule</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.</p><div id=\"rules-container\" class=\"space-y-3\"></div><div class=\"mt-4 flex items-center gap-2\"><label for=\"match-type\" class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Match:</label> <select id=\"match-type\" name=\"match_type\" class=\"input w-auto\"><option value=\"all\">ALL rules (AND)</option> <option value=\"any\">ANY rule (OR)</option></select></div></div><div class=\"card p-6\"><h2 class=\"text-lg font-semibold mb-4 flex items-center gap-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Manual Book Selection</h2><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Add specific books to this section. Use the search to find and select multiple books.</p><div class=\"mb-4\"><label for=\"book-search\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Search Books</label><div class=\"flex gap-2\"><input type=\"text\" id=\"book-search\" name=\"book_search\" class=\"input flex-1\" placeholder=\"Search by title or author…\" autocomplete=\"off\"> <button type=\"button\" id=\"search-books-btn\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Search</button></div></div><div id=\"search-results\" class=\"hidden mb-4 card p-3 max-h-60 overflow-y-auto space-y-2\" style=\"background-color: var(--bg-primary);\"></div><div class=\"mb-4\"><label class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Selected Books</label><div id=\"selected-books\" class=\"min-h-[60px] p-3 rounded-xl border-2 border-dashed\" style=\"border-color: var(--border-strong); background-color: var(--bg-primary);\"><p class=\"text-sm text-center\" style=\"color: var(--text-secondary);\">No books selected</p></div></div></div><div class=\"card p-6\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Live Preview</h2><button type=\"button\" id=\"preview-btn\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Refresh Preview</button></div><div id=\"preview-container\" class=\"card p-4\" style=\"background-color: var(--bg-primary); min-height: 200px;\"><p class=\"text-center text-sm\" style=\"color: var(--text-secondary);\">Add filter rules or select books to see a preview of your custom section.</p></div></div><div class=\"flex justify-end gap-3\"><button type=\"button\" id=\"cancel-btn\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" id=\"save-section-btn\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Save Section</button></div></form></main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -81,7 +153,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+133 -69
View File
@@ -46,7 +46,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Collections Container --><main id=\"collections-container\" class=\"w-full px-4 py-8\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main id=\"collections-container\" class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Your Library</h1><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Pick up where you left off, or explore something new.</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -56,7 +56,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main><!-- Dashboard Settings Modal -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -97,14 +97,14 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
templ_7745c5c3_Var2 = templ.NopComponent templ_7745c5c3_Var2 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"dashboard-collection mb-8\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"dashboard-collection mb-10\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 37, Col: 33} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 39, Col: 33}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -117,33 +117,33 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 38, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 40, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><!-- Collection Header --><div class=\"flex items-center justify-between mb-4\"><div class=\"flex items-center gap-3\"><span class=\"text-2xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"flex items-end justify-between mb-4\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-9 w-9 rounded-lg text-lg\" style=\"background-color: var(--accent-muted);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 43, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 44, Col: 130}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span><div><h2 class=\"text-lg font-bold tracking-tight\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 45, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 46, Col: 100}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -154,14 +154,14 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if section.Description != "" { if section.Description != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-xs\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 47, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 48, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -184,73 +184,113 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
var templ_7745c5c3_Var8 templ.SafeURL var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(section.ViewAllURL) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(section.ViewAllURL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 53, Col: 30} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 54, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All →</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("arrow-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><!-- Carousel --><div class=\"carousel-container relative group\"><button class=\"carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 w-12 h-full bg-gradient-to-r from-gray-900 to-transparent flex items-center justify-start opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div><div class=\"carousel-container relative group\"><button class=\"carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 69, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 67, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-direction=\"-1\" aria-label=\"Scroll left\"><span class=\"text-3xl pl-2\" style=\"color: var(--text-primary);\"></span></button><div id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-direction=\"-1\" aria-label=\"Scroll left\" style=\"background: linear-gradient(to right, var(--bg-primary), transparent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-left", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button><div id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("carousel-track-" + section.ID) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("carousel-track-" + section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 76, Col: 39} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 75, Col: 39}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-12 pb-4\" style=\"scrollbar-width: none; -ms-overflow-style: none;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"carousel-track flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory px-6 pb-2\" style=\"scrollbar-width: none; -ms-overflow-style: none;\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range section.Items { for _, item := range section.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"flex-shrink-0 w-36 sm:w-40 snap-start\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = BookCard(item).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = BookCard(item).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} }
if len(section.Items) == 0 { if len(section.Items) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"text-center py-8 w-full\" style=\"color: var(--text-secondary);\"><p>No items in this collection</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"flex flex-col items-center justify-center text-center py-12 w-full gap-2\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book", "h-8 w-8 opacity-50").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"text-sm\">No items in this collection</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><button class=\"carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 w-12 h-full bg-gradient-to-l from-gray-900 to-transparent flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><button class=\"carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10 h-full w-12 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200\" data-action=\"scroll-carousel\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 97, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 94, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" data-direction=\"1\" aria-label=\"Scroll right\"><span class=\"text-3xl pr-2\" style=\"color: var(--text-primary);\"></span></button></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-direction=\"1\" aria-label=\"Scroll right\" style=\"background: linear-gradient(to left, var(--bg-primary), transparent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-right", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</button></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -279,113 +319,121 @@ func BookCard(item handlers.BookInfo) templ.Component {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var12 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 templ.SafeURL var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 39} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 106, Col: 39}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" title=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 106, Col: 60}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><div class=\"book-card flex-shrink-0 w-36 rounded-lg overflow-hidden snap-start cursor-pointer transition-transform duration-200 hover:scale-105\" tabindex=\"0\" role=\"button\" aria-label=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"block h-full\"><div class=\"book-card w-full h-full rounded-xl overflow-hidden cursor-pointer\" tabindex=\"0\" role=\"button\" aria-label=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("View " + item.Title) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("View " + item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 111, Col: 36}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><div class=\"aspect-[2/3] overflow-hidden shadow-lg bg-gradient-to-br from-gray-700 to-gray-900\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\"><div class=\"book-card-cover aspect-[2/3] overflow-hidden\" style=\"background-color: color-mix(in srgb, var(--text-primary) 8%, transparent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.CoverImagePath != "" { if item.CoverImagePath != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<img src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<img src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 122, Col: 31} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 119, Col: 31}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" alt=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 123, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 120, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<img src=\"/static/placeholder-book.svg\" alt=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<img src=\"/static/placeholder-book.svg\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 128, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"w-full h-full object-cover\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-full h-full object-cover\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div><div class=\"book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_40%,transparent)]\"><h3 class=\"font-semibold text-base line-clamp-2\" style=\"color: var(--text-primary)\" title=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"book-card-action\"><span class=\"book-card-action-btn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-5 w-5 translate-x-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div></div><div class=\"book-card-meta\"><h3 class=\"font-semibold text-sm leading-snug line-clamp-2\" style=\"color: var(--text-primary)\" title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 137, Col: 106} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 137, Col: 117}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -398,25 +446,25 @@ func BookCard(item handlers.BookInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</h3>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h3>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.Author != "" { if item.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<p class=\"text-sm line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<p class=\"text-xs mt-0.5 line-clamp-1\" style=\"color: var(--text-secondary)\" title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 141, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 141, Col: 100}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -429,12 +477,12 @@ func BookCard(item handlers.BookInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div></div></a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -463,138 +511,154 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
templ_7745c5c3_Var23 = templ.NopComponent templ_7745c5c3_Var23 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"rounded-lg p-6 w-full max-w-2xl mx-4 shadow-2xl\" style=\"background-color: var(--bg-secondary);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><button data-action=\"close-dashboard-settings\" class=\"p-2 hover:bg-gray-700 rounded transition-colors\">✕</button></div><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary);\">Drag to reorder collections, toggle visibility with the switch.</p><!-- Draggable Collection List --><div id=\"collection-list\" class=\"space-y-2 mb-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div id=\"dashboard-settings-modal\" class=\"hidden fixed inset-0 z-[70] flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl shadow-2xl\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Customize Dashboard</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary)\">Drag to reorder, toggle to show or hide.</p></div><button data-action=\"close-dashboard-settings\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></div><div id=\"collection-list\" class=\"space-y-2 mb-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, section := range sections { for _, section := range sections {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"collection-item flex items-center justify-between p-3 rounded border cursor-move select-none\" data-collection-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<div class=\"collection-item flex items-center justify-between p-3 rounded-xl cursor-move select-none card\" data-collection-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 178, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 170, Col: 37}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" data-is-system=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" data-is-system=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem)) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 179, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 171, Col: 58}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" draggable=\"true\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><div class=\"flex items-center gap-3\"><span class=\"text-xl\" style=\"color: var(--text-secondary);\">☰</span> <span class=\"text-xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" draggable=\"true\"><div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("grip", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"text-xl\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 43} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 176, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span><div><span class=\"font-medium\" style=\"color: var(--text-primary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span><div class=\"flex items-center gap-2\"><span class=\"font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 187, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 178, Col: 84}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if section.IsSystem { if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span class=\"text-xs ml-2 px-2 py-1 rounded\" style=\"background-color: var(--accent);\">System</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">System</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div></div><div class=\"flex items-center gap-3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div></div><div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if section.IsSystem { if section.IsSystem {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<button data-action=\"restore-system-collection\" data-collection-name=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<button data-action=\"restore-system-collection\" data-collection-name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 197, Col: 42} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 188, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"text-xs px-3 py-1 rounded border hover:opacity-80 transition-opacity\" style=\"border-color: var(--border); color: var(--text-secondary);\" title=\"Restore { section.Title } to defaults\">Restore</button> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" class=\"btn btn-secondary py-1 px-2.5 text-xs\" title=\"Restore { section.Title } to defaults\">Restore</button> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<label class=\"relative inline-flex items-center cursor-pointer\"><input type=\"checkbox\" class=\"sr-only peer\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if !ContainsString(hiddenCollections, section.ID) { if !ContainsString(hiddenCollections, section.ID) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " checked") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " checked")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "><div class=\"w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600\"></div></label></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "><div class=\"w-11 h-6 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all\" style=\"background-color: var(--border-strong);\"></div></label></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div><!-- Items Per Section Slider --><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Items per Collection: <span id=\"items-count-display\" class=\"font-bold\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Items per Collection: <span id=\"items-count-display\" class=\"font-bold\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection) templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 214, Col: 128}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></label> <input type=\"range\" min=\"10\" max=\"50\" step=\"5\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 221, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" class=\"w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer\" data-input-action=\"update-items-count\" target=\"items-count-display\"></div><div class=\"flex justify-end gap-3\"><button data-action=\"close-dashboard-settings\" class=\"px-4 py-2 rounded-lg border hover:bg-gray-700 transition-colors\" style=\"border-color: var(--border); color: var(--text-primary);\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"px-4 py-2 rounded-lg text-white font-medium hover:opacity-90 transition-opacity\" style=\"background-color: var(--accent);\">Save Changes</button></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"w-full h-2 rounded-lg appearance-none cursor-pointer\" style=\"background-color: var(--border-strong);\" data-input-action=\"update-items-count\" target=\"items-count-display\"></div><div class=\"flex justify-end gap-3\"><button data-action=\"close-dashboard-settings\" class=\"btn btn-secondary\">Cancel</button> <button data-action=\"save-dashboard-settings\" class=\"btn btn-primary\">Save Changes</button></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+110 -140
View File
@@ -14,55 +14,57 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</head> </head>
<body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()"> <body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()">
@Header(user, "/devices") @Header(user, "/devices")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<!-- Header Section -->
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center gap-4 flex-wrap">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("device", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Device Management</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
</div> </div>
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg"> <button @click="showAddDeviceModal()" class="btn btn-primary">
Add New Device @Icon("plus", "h-4 w-4")
Add New Device
</button> </button>
</div> </div>
</div> </div>
<!-- Devices Grid -->
<div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Empty state -->
if len(devices) == 0 { if len(devices) == 0 {
<div id="empty-state" class="text-center py-16 col-span-full" style="color: var(--text-secondary)"> <div id="empty-state" class="card text-center py-16 col-span-full">
<div class="text-6xl mb-4">📱</div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("device", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3>
<p class="mb-4">Add your reading devices to enable cross-device sync</p> <p class="mb-4 text-sm" style="color: var(--text-secondary)">Add your reading devices to enable cross-device sync</p>
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg"> <button @click="showAddDeviceModal()" class="btn btn-primary">Add Your First Device</button>
Add Your First Device
</button>
</div> </div>
} }
<!-- Devices Grid -->
if len(devices) > 0 { if len(devices) > 0 {
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="devices-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 col-span-full">
for _, device := range devices { for _, device := range devices {
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6">
<div class="flex items-start justify-between mb-4"> <div class="flex items-start justify-between mb-4">
<div class="text-4xl"> <span class="grid place-items-center h-11 w-11 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
if device.DeviceType == "koreader" { if device.DeviceType == "koreader" {
📖 @Icon("book-open", "h-5 w-5")
} else if device.DeviceType == "kobo" { } else if device.DeviceType == "kobo" {
📚 @Icon("library", "h-5 w-5")
} else if device.DeviceType == "web" { } else if device.DeviceType == "web" {
🌐 @Icon("globe", "h-5 w-5")
} else { } else {
📱 @Icon("device", "h-5 w-5")
} }
</div> </span>
<div class="flex space-x-2"> <div class="flex space-x-1">
<button @click={ "showShelfMappings('" + device.ID.String() + "')" } class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);"> <button @click={ "showShelfMappings('" + device.ID.String() + "')" } class="icon-btn" title="Shelf mappings" aria-label="Shelf mappings">
📚 @Icon("layers", "h-5 w-5")
</button> </button>
<button @click={ "showDeviceSettings('" + device.ID.String() + "')" } class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);"> <button @click={ "showDeviceSettings('" + device.ID.String() + "')" } class="icon-btn" title="Device settings" aria-label="Device settings">
⚙️ @Icon("settings", "h-5 w-5")
</button> </button>
</div> </div>
</div> </div>
@@ -72,9 +74,9 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<div class="flex justify-between"> <div class="flex justify-between">
<span style="color: var(--text-secondary)">Sync Status</span> <span style="color: var(--text-secondary)">Sync Status</span>
if device.SyncEnabled { if device.SyncEnabled {
<span style="color: var(--accent)"> Enabled</span> <span class="badge status-completed">Enabled</span>
} else { } else {
<span style="color: var(--text-secondary)"> Disabled</span> <span class="badge" style="background-color: var(--surface-hover); color: var(--text-secondary);">Disabled</span>
} }
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@@ -82,7 +84,7 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
if device.LastSync != nil { if device.LastSync != nil {
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSync, user.Timezone) }</span> <span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSync, user.Timezone) }</span>
} else { } else {
<span style="color: var(--text-primary)">Never</span> <span style="color: var(--text-secondary)">Never</span>
} }
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@@ -90,15 +92,13 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
if device.LastSeen != nil { if device.LastSeen != nil {
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSeen, user.Timezone) }</span> <span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSeen, user.Timezone) }</span>
} else { } else {
<span style="color: var(--text-primary)">Never</span> <span style="color: var(--text-secondary)">Never</span>
} }
</div> </div>
</div> </div>
<!-- NEW: Sync URL & Token Management -->
<div class="border-t pt-4" style="border-color: var(--border);"> <div class="border-t pt-4" style="border-color: var(--border);">
<p class="text-xs font-semibold mb-2" style="color: var(--text-secondary)">DEVICE SYNC CONFIGURATION</p> <p class="text-xs font-semibold uppercase tracking-wide mb-3" style="color: var(--text-secondary)">Device Sync Configuration</p>
if device.DeviceType == "kobo" { if device.DeviceType == "kobo" {
<!-- Kobo: Copy Full Sync URL -->
<div class="mb-3"> <div class="mb-3">
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Kobo Sync URL</label> <label class="block text-xs mb-1" style="color: var(--text-secondary)">Kobo Sync URL</label>
<div class="flex space-x-2"> <div class="flex space-x-2">
@@ -107,22 +107,20 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
id="sync-url-{ device.ID }" id="sync-url-{ device.ID }"
readonly readonly
value={ baseURL + "/api/sync/kobo/" + device.AuthToken } value={ baseURL + "/api/sync/kobo/" + device.AuthToken }
class="flex-1 px-3 py-2 text-xs rounded border" class="input text-xs font-mono"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
<button <button
@click="copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL')" @click="copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL')"
class="px-3 py-2 text-xs rounded hover:opacity-80" class="btn btn-primary text-xs px-3"
style="background-color: var(--accent); color: white;"
> >
📋 Copy @Icon("copy", "h-4 w-4")
Copy
</button> </button>
</div> </div>
<p class="text-xs mt-1" style="color: var(--text-secondary);">Paste this URL into Kobo's <code class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary);">api_endpoint</code> setting</p> <p class="text-xs mt-1" style="color: var(--text-secondary);">Paste this URL into Kobo's <code class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary);">api_endpoint</code> setting</p>
</div> </div>
} }
if device.DeviceType == "koreader" { if device.DeviceType == "koreader" {
<!-- KOReader: Copy Auth Token -->
<div class="mb-3"> <div class="mb-3">
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Auth Token (for plugin)</label> <label class="block text-xs mb-1" style="color: var(--text-secondary)">Auth Token (for plugin)</label>
<div class="flex space-x-2"> <div class="flex space-x-2">
@@ -131,43 +129,40 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
id="auth-token-{ device.ID }" id="auth-token-{ device.ID }"
readonly readonly
value={ device.AuthToken } value={ device.AuthToken }
class="flex-1 px-3 py-2 text-xs rounded border" class="input text-xs font-mono"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border); font-family: monospace;"
/> />
<button <button
@click="copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token')" @click="copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token')"
class="px-3 py-2 text-xs rounded hover:opacity-80" class="btn btn-primary text-xs px-3"
style="background-color: var(--accent); color: white;"
> >
📋 Copy @Icon("copy", "h-4 w-4")
Copy
</button> </button>
</div> </div>
<p class="text-xs mt-1" style="color: var(--text-secondary);">Enter this token in the KOReader plugin settings</p> <p class="text-xs mt-1" style="color: var(--text-secondary);">Enter this token in the KOReader plugin settings</p>
</div> </div>
} }
<!-- Regenerate Token Button -->
<button <button
hx-put={ "/api/devices/" + device.ID.String() + "/regenerate-token" } hx-put={ "/api/devices/" + device.ID.String() + "/regenerate-token" }
hx-confirm="⚠️ Old token will immediately stop working. Regenerate anyway?" hx-confirm="Old token will immediately stop working. Regenerate anyway?"
class="w-full px-3 py-2 text-xs rounded border hover:opacity-80" class="btn btn-secondary w-full text-xs"
style="border-color: var(--border); color: var(--text-secondary); background-color: var(--bg-primary);"
> >
🔄 Regenerate Token @Icon("refresh", "h-4 w-4")
Regenerate Token
</button> </button>
<p class="text-xs mt-1" style="color: var(--text-secondary);">⚠️ Old token will immediately stop working</p> <p class="text-xs mt-1" style="color: var(--status-warning);">Old token will immediately stop working</p>
</div> </div>
</div> </div>
} }
</div> </div>
} }
</div> </div>
<!-- Pending Registrations -->
if len(pendingRegistrations) > 0 { if len(pendingRegistrations) > 0 {
<div id="pending-section" class="mt-12"> <div id="pending-section" class="mt-12">
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2> <h2 class="text-lg font-bold tracking-tight mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
<div class="space-y-4"> <div class="space-y-4">
for _, reg := range pendingRegistrations { for _, reg := range pendingRegistrations {
<div class="card p-4 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-4 flex items-center justify-between gap-4 flex-wrap">
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3> <h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3>
<p class="text-sm" style="color: var(--text-secondary)"> <p class="text-sm" style="color: var(--text-secondary)">
@@ -175,10 +170,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</p> </p>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<button @click={ "approveDevice('" + reg.RegistrationID + "')" } class="btn-primary px-4 py-2 rounded-lg text-sm"> <button @click={ "approveDevice('" + reg.RegistrationID + "')" } class="btn btn-primary text-sm">
Approve Approve
</button> </button>
<button @click={ "rejectDevice('" + reg.RegistrationID + "')" } class="btn-danger px-4 py-2 rounded-lg text-sm"> <button @click={ "rejectDevice('" + reg.RegistrationID + "')" } class="btn btn-danger text-sm">
Reject Reject
</button> </button>
</div> </div>
@@ -187,43 +182,39 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</div> </div>
</div> </div>
} }
<!-- Sync Queue Section -->
<div id="sync-queue-section" class="mt-12 hidden"> <div id="sync-queue-section" class="mt-12 hidden">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Queue</h2> <h2 class="text-lg font-bold tracking-tight" style="color: var(--text-primary)">Sync Queue</h2>
<button @click="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg"> <button @click="clearSyncQueue()" class="btn btn-secondary">Clear Queue</button>
Clear Queue
</button>
</div> </div>
<div id="sync-queue" class="space-y-3"></div> <div id="sync-queue" class="space-y-3"></div>
</div> </div>
</div> </div>
<!-- Add Device Modal --> <div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
<button @click="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button @click="hideAddDeviceModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<form id="add-device-form" @submit="handleAddDevice($event)"> <form id="add-device-form" @submit="handleAddDevice($event)">
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Name</label>
<input <input
type="text" type="text"
id="device-name" id="device-name"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="My Kindle Paperwhite" placeholder="My Kindle Paperwhite"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Type</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Type</label>
<select <select
id="device-type" id="device-type"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="">Select device type...</option> <option value="">Select device type...</option>
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option> <option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
@@ -233,46 +224,41 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</select> </select>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Identifier</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Identifier</label>
<input <input
type="text" type="text"
id="device-identifier" id="device-identifier"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="Hardware ID or Serial Number" placeholder="Hardware ID or Serial Number"
/> />
<p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p> <p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="hideAddDeviceModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideAddDeviceModal()" class="btn btn-secondary">Cancel</button>
Cancel <button type="submit" class="btn btn-primary">Register Device</button>
</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
Register Device
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Device Settings Modal --> <div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2>
<button @click="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button @click="hideDeviceSettingsModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<form id="device-settings-form" @submit="handleSaveDeviceSettings($event)"> <form id="device-settings-form" @submit="handleSaveDeviceSettings($event)">
<input type="hidden" id="settings-device-id"/> <input type="hidden" id="settings-device-id"/>
<input type="hidden" id="settings-device-type"/> <input type="hidden" id="settings-device-type"/>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Name</label>
<input <input
type="text" type="text"
id="settings-device-name" id="settings-device-name"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
@@ -300,11 +286,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p> <p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Frequency</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
<select <select
id="settings-sync-frequency" id="settings-sync-frequency"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="1">Every 1 minute</option> <option value="1">Every 1 minute</option>
<option value="5" selected>Every 5 minutes</option> <option value="5" selected>Every 5 minutes</option>
@@ -314,14 +299,13 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</select> </select>
</div> </div>
<div class="border-t pt-6 mb-6" style="border-color: var(--border);"> <div class="border-t pt-6 mb-6" style="border-color: var(--border);">
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Collection View Settings</h3> <h3 class="text-base font-semibold mb-1" style="color: var(--text-primary)">Collection View Settings</h3>
<p class="text-sm mb-4" style="color: var(--text-secondary)">Configure how collections are displayed on this device</p> <p class="text-sm mb-4" style="color: var(--text-secondary)">Configure how collections are displayed on this device</p>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Default View Mode</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Default View Mode</label>
<select <select
id="settings-view-mode" id="settings-view-mode"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="grid">Grid View</option> <option value="grid">Grid View</option>
<option value="list">List View</option> <option value="list">List View</option>
@@ -329,11 +313,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</select> </select>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sort Collections By</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sort Collections By</label>
<select <select
id="settings-sort-order" id="settings-sort-order"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="name">Collection Name</option> <option value="name">Collection Name</option>
<option value="created">Date Created</option> <option value="created">Date Created</option>
@@ -342,11 +325,10 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</select> </select>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Items Per Page</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Items Per Page</label>
<select <select
id="settings-items-per-page" id="settings-items-per-page"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="12">12 items</option> <option value="12">12 items</option>
<option value="24" selected>24 items</option> <option value="24" selected>24 items</option>
@@ -378,81 +360,73 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display progress indicators for books</p> <p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display progress indicators for books</p>
</div> </div>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3 flex-wrap">
<button type="button" @click="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideDeviceSettingsModal()" class="btn btn-secondary">Cancel</button>
Cancel <button type="button" @click="handleRevokeDevice()" class="btn btn-danger">Revoke Device</button>
</button> <button type="submit" class="btn btn-primary">Save Settings</button>
<button type="button" @click="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg">
Revoke Device
</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
Save Settings
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Device Shelf Mappings Modal --> <div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div class="card p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto" style="box-shadow: var(--shadow-pop);">
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Collection to Shelf Mappings</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Collection to Shelf Mappings</h2>
<button @click="hideShelfMappingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button @click="hideShelfMappingsModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<input type="hidden" id="mappings-device-id"/> <input type="hidden" id="mappings-device-id"/>
<div id="shelf-mappings-container" class="space-y-4 mb-6"> <div id="shelf-mappings-container" class="space-y-4 mb-6">
<p style="color: var(--text-secondary)">Loading mappings...</p> <p style="color: var(--text-secondary)">Loading mappings...</p>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="hideShelfMappingsModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideShelfMappingsModal()" class="btn btn-secondary">Close</button>
Close <button type="button" @click="showAddMappingModal()" class="btn btn-primary">
</button> @Icon("plus", "h-4 w-4")
<button type="button" @click="showAddMappingModal()" class="btn-primary px-4 py-2 rounded-lg"> Add Mapping
Add Mapping
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Add/Edit Shelf Mapping Modal --> <div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Collection Mapping</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Collection Mapping</h2>
<button @click="hideAddMappingModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button @click="hideAddMappingModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<form id="mapping-form" @submit="handleSaveMapping($event)"> <form id="mapping-form" @submit="handleSaveMapping($event)">
<input type="hidden" id="mapping-device-id"/> <input type="hidden" id="mapping-device-id"/>
<input type="hidden" id="mapping-id"/> <input type="hidden" id="mapping-id"/>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Collection</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Collection</label>
<select <select
id="mapping-collection" id="mapping-collection"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="">Select collection...</option> <option value="">Select collection...</option>
</select> </select>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Shelf Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device Shelf Name</label>
<input <input
type="text" type="text"
id="mapping-shelf-name" id="mapping-shelf-name"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
placeholder="e.g., Sci-Fi" placeholder="e.g., Sci-Fi"
/> />
<p class="text-xs mt-1" style="color: var(--text-secondary)">Name of the shelf on this device</p> <p class="text-xs mt-1" style="color: var(--text-secondary)">Name of the shelf on this device</p>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Direction</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Direction</label>
<select <select
id="mapping-sync-direction" id="mapping-sync-direction"
required required
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="bidirectional">Bidirectional (sync both ways)</option> <option value="bidirectional">Bidirectional (sync both ways)</option>
<option value="book_to_device">Bookhoard Device</option> <option value="book_to_device">Bookhoard Device</option>
@@ -461,12 +435,8 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</select> </select>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button type="button" @click="hideAddMappingModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideAddMappingModal()" class="btn btn-secondary">Cancel</button>
Cancel <button type="submit" class="btn btn-primary">Save Mapping</button>
</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
Save Mapping
</button>
</div> </div>
</form> </form>
</div> </div>
File diff suppressed because one or more lines are too long
+84 -83
View File
@@ -12,101 +12,102 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<link href="/static/style.css" rel="stylesheet"/> <link href="/static/style.css" rel="stylesheet"/>
<link rel="stylesheet" href="/static/highlight-dark.min.css"/> <link rel="stylesheet" href="/static/highlight-dark.min.css"/>
</head> </head>
<body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased" }> <body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs font-sans antialiased" }>
@Header(user, currentPath) @Header(user, currentPath)
<!-- Mobile Menu Button -->
<button <button
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80" class="lg:hidden icon-btn fixed top-[4.75rem] left-4 z-50 bg-surface-raised"
onclick="toggleSidebar()" onclick="toggleSidebar()"
aria-label="Toggle menu" aria-label="Toggle documentation menu"
> >
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> @Icon("menu", "h-5 w-5")
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button> </button>
<!-- Search Results Overlay --> <div id="docs-overlay" class="hidden fixed inset-0 z-40 lg:hidden" style="background: var(--surface-overlay);" onclick="toggleSidebar()"></div>
<div id="search-results" class="hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out"></div> <div id="search-results" class="hidden fixed inset-0 z-50 overflow-y-auto p-8" style="background: var(--surface-overlay);"></div>
<!-- Sidebar --> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out"> <div class="flex gap-8 py-8">
<!-- Search --> <aside id="docs-sidebar" class="sidebar fixed lg:sticky top-16 lg:top-20 bottom-0 lg:bottom-auto left-0 lg:left-auto z-40 lg:z-10 w-64 shrink-0 lg:h-[calc(100vh-6rem)] overflow-y-auto -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out bg-surface-raised border-r border-line lg:border-r-0">
<div class="p-4 border-b border-border"> <div class="p-4 border-b border-line">
<input <div class="relative">
type="text" <span class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" style="color: var(--text-secondary);">
class="search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm" @Icon("search", "h-4 w-4")
placeholder="Search documentation..." </span>
id="docs-search" <input
/> type="text"
</div> class="search-input input pl-9"
<!-- Navigation Sections --> placeholder="Search documentation…"
for _, section := range nav.Sections { id="docs-search"
<div class="nav-section mb-6"> />
<div </div>
class="nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between"
@click="toggleSection($el)"
>
<span>{ section.Title }</span>
<svg class="w-4 h-4 transform transition-transform section-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div> </div>
if section.Collapsed { for _, section := range nav.Sections {
<div class="nav-items hidden" data-collapsed="true"> <div class="nav-section border-b border-line">
for _, item := range section.Items { <div
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline"> class="nav-section-title flex items-center justify-between px-4 py-3 cursor-pointer select-none text-xs uppercase tracking-wider font-semibold transition-colors hover:bg-surface-hover"
<span class="mr-2">{ item.Icon }</span>{ item.Title } style="color: var(--text-secondary);"
</a> @click="toggleSection($el)"
} >
</div> <span>{ section.Title }</span>
} else { @Icon("chevron-down", "h-4 w-4 section-arrow transition-transform")
<div class="nav-items" data-collapsed="false"> </div>
for _, item := range section.Items { if section.Collapsed {
<a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline"> <div class="nav-items hidden" data-collapsed="true">
<span class="mr-2">{ item.Icon }</span>{ item.Title } for _, item := range section.Items {
</a> <a href={ item.URL } class="nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
<span class="text-xs" style="color: var(--accent);">{ item.Icon }</span>
<span>{ item.Title }</span>
</a>
}
</div>
} else {
<div class="nav-items" data-collapsed="false">
for _, item := range section.Items {
<a href={ item.URL } class="nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
<span class="text-xs" style="color: var(--accent);">{ item.Icon }</span>
<span>{ item.Title }</span>
</a>
}
</div>
} }
</div> </div>
} }
</div> </aside>
} <main class="main-content flex-1 min-w-0 max-w-4xl">
</div> if len(doc.Breadcrumb) > 0 {
<!-- Main Content --> <nav class="breadcrumb flex flex-wrap items-center gap-1.5 text-sm mb-8" aria-label="Breadcrumb" style="color: var(--text-secondary);">
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto"> for i, crumb := range doc.Breadcrumb {
<!-- Breadcrumb --> if i > 0 {
if len(doc.Breadcrumb) > 0 { @Icon("chevron-right", "h-4 w-4")
<nav class="breadcrumb flex gap-2 text-sm text-text-secondary mb-8" aria-label="Breadcrumb"> }
for i, crumb := range doc.Breadcrumb { <a href={ crumb.URL } class="no-underline transition-colors hover:underline" style="color: var(--accent);">{ crumb.Title }</a>
if i > 0 { }
<span class="text-text-secondary/50"></span> </nav>
}
<a href={ crumb.URL } class="text-accent hover:underline no-underline">{ crumb.Title }</a>
} }
</nav> <h1 class="text-4xl font-bold mb-8 tracking-tight" style="color: var(--text-primary);">{ doc.Title }</h1>
} if len(doc.TOC) > 0 {
<!-- Title --> <details class="toc card p-4 mb-8">
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1> <summary class="cursor-pointer list-none [&::-webkit-details-marker]:hidden flex items-center gap-2 font-semibold transition-colors hover:bg-surface-hover -m-4 p-4 rounded-2xl" style="color: var(--text-primary);">
<!-- Table of Contents --> <span>On this page</span>
if len(doc.TOC) > 0 { @Icon("chevron-down", "h-4 w-4 ml-auto")
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border"> </summary>
<summary class="cursor-pointer hover:opacity-80 transition-opacity"> <div class="mt-3">
<strong class="text-text-primary font-semibold">On this page</strong> for _, item := range doc.TOC {
<span class="ml-2 text-text-secondary text-xs"></span> <a
</summary> href={ "#" + item.Anchor }
for _, item := range doc.TOC { class="toc-item block py-1 text-sm no-underline transition-colors hover:text-brand"
<a style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);" }
href={ "#" + item.Anchor } >
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline" { item.Title }
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) } </a>
> }
{ item.Title } </div>
</a> </details>
} }
</details> <div class="prose prose-invert max-w-none">
} @UnsafeHTML(doc.Content).ToComponent()
<!-- Content --> </div>
<div class="prose prose-invert max-w-none"> </main>
@UnsafeHTML(doc.Content).ToComponent()
</div> </div>
</main> </div>
</body> </body>
</html> </html>
} }
+80 -48
View File
@@ -48,7 +48,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " page-docs bg-background-primary text-text-primary font-sans antialiased"} var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " page-docs font-sans antialiased"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@@ -74,267 +74,299 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!-- Mobile Menu Button --><button class=\"lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2 text-text-primary hover:bg-background-secondary/80\" onclick=\"toggleSidebar()\" aria-label=\"Toggle menu\"><svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button><!-- Search Results Overlay --><div id=\"search-results\" class=\"hidden fixed inset-0 bg-black/95 z-50 overflow-y-auto p-8 transition-opacity duration-200 ease-in-out\"></div><!-- Sidebar --><div class=\"sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary border-r border-border overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out\"><!-- Search --><div class=\"p-4 border-b border-border\"><input type=\"text\" class=\"search-input w-full px-3 py-2 rounded bg-background-primary text-text-primary border border-border focus:outline-none focus:ring-2 focus:ring-accent text-sm\" placeholder=\"Search documentation...\" id=\"docs-search\"></div><!-- Navigation Sections -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button class=\"lg:hidden icon-btn fixed top-[4.75rem] left-4 z-50 bg-surface-raised\" onclick=\"toggleSidebar()\" aria-label=\"Toggle documentation menu\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("menu", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button><div id=\"docs-overlay\" class=\"hidden fixed inset-0 z-40 lg:hidden\" style=\"background: var(--surface-overlay);\" onclick=\"toggleSidebar()\"></div><div id=\"search-results\" class=\"hidden fixed inset-0 z-50 overflow-y-auto p-8\" style=\"background: var(--surface-overlay);\"></div><div class=\"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8\"><div class=\"flex gap-8 py-8\"><aside id=\"docs-sidebar\" class=\"sidebar fixed lg:sticky top-16 lg:top-20 bottom-0 lg:bottom-auto left-0 lg:left-auto z-40 lg:z-10 w-64 shrink-0 lg:h-[calc(100vh-6rem)] overflow-y-auto -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out bg-surface-raised border-r border-line lg:border-r-0\"><div class=\"p-4 border-b border-line\"><div class=\"relative\"><span class=\"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> <input type=\"text\" class=\"search-input input pl-9\" placeholder=\"Search documentation…\" id=\"docs-search\"></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, section := range nav.Sections { for _, section := range nav.Sections {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"nav-section mb-6\"><div class=\"nav-section-title font-semibold text-xs uppercase tracking-wider text-text-secondary px-4 py-3 cursor-pointer select-none flex items-center justify-between\" @click=\"toggleSection($el)\"><span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"nav-section border-b border-line\"><div class=\"nav-section-title flex items-center justify-between px-4 py-3 cursor-pointer select-none text-xs uppercase tracking-wider font-semibold transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" @click=\"toggleSection($el)\"><span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 47, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 49, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> <svg class=\"w-4 h-4 transform transition-transform section-arrow\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path></svg></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-down", "h-4 w-4 section-arrow transition-transform").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if section.Collapsed { if section.Collapsed {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"nav-items hidden\" data-collapsed=\"true\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"nav-items hidden\" data-collapsed=\"true\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range section.Items { for _, item := range section.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 templ.SafeURL var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 55, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 55, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\"><span class=\"mr-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\"><span class=\"text-xs\" style=\"color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 40} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 75}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 57, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"nav-items\" data-collapsed=\"false\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"nav-items\" data-collapsed=\"false\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range section.Items { for _, item := range section.Items {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 templ.SafeURL var templ_7745c5c3_Var9 templ.SafeURL
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 63, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"nav-item block py-2 px-4 pl-8 text-text-secondary hover:text-accent text-sm transition-colors no-underline\"><span class=\"mr-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"nav-item flex items-center gap-2 py-2 pl-8 pr-4 text-sm no-underline transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\"><span class=\"text-xs\" style=\"color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 40} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 65, Col: 75}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> <span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 64, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 66, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><!-- Main Content --><main class=\"main-content lg:ml-72 p-8 max-w-4xl mx-auto\"><!-- Breadcrumb -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside><main class=\"main-content flex-1 min-w-0 max-w-4xl\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(doc.Breadcrumb) > 0 { if len(doc.Breadcrumb) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<nav class=\"breadcrumb flex gap-2 text-sm text-text-secondary mb-8\" aria-label=\"Breadcrumb\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<nav class=\"breadcrumb flex flex-wrap items-center gap-1.5 text-sm mb-8\" aria-label=\"Breadcrumb\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for i, crumb := range doc.Breadcrumb { for i, crumb := range doc.Breadcrumb {
if i > 0 { if i > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"text-text-secondary/50\"></span>") templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " <a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " <a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 templ.SafeURL var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 26} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"text-accent hover:underline no-underline\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"no-underline transition-colors hover:underline\" style=\"color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 91} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 81, Col: 129}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</nav>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</nav>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<!-- Title --><h1 class=\"text-4xl font-bold mb-8 text-text-primary\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<h1 class=\"text-4xl font-bold mb-8 tracking-tight\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 86, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 85, Col: 104}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</h1><!-- Table of Contents -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</h1>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(doc.TOC) > 0 { if len(doc.TOC) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<details class=\"toc bg-background-secondary p-4 rounded-lg mb-8 border border-border\"><summary class=\"cursor-pointer hover:opacity-80 transition-opacity\"><strong class=\"text-text-primary font-semibold\">On this page</strong> <span class=\"ml-2 text-text-secondary text-xs\">▼</span></summary> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<details class=\"toc card p-4 mb-8\"><summary class=\"cursor-pointer list-none [&::-webkit-details-marker]:hidden flex items-center gap-2 font-semibold transition-colors hover:bg-surface-hover -m-4 p-4 rounded-2xl\" style=\"color: var(--text-primary);\"><span>On this page</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-down", "h-4 w-4 ml-auto").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</summary><div class=\"mt-3\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, item := range doc.TOC { for _, item := range doc.TOC {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 templ.SafeURL var templ_7745c5c3_Var15 templ.SafeURL
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 96, Col: 32} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 95, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline\" style=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"toc-item block py-1 text-sm no-underline transition-colors hover:text-brand\" style=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level)) templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 98, Col: 66} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 97, Col: 105}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 100, Col: 20} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 99, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</details>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div></details>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<!-- Content --><div class=\"prose prose-invert max-w-none\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"prose prose-invert max-w-none\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -342,7 +374,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></main></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></main></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+5 -6
View File
@@ -1,16 +1,14 @@
package templates package templates
// FilterItem renders a single saved filter item for the list
templ FilterItem(id string, name string) { templ FilterItem(id string, name string) {
<div <div
class="flex items-center justify-between p-2 rounded hover:opacity-80" class="flex items-center justify-between gap-2 p-2 rounded-lg hover:bg-surface-hover"
style="background-color: var(--bg-primary);"
data-filter-id={ id } data-filter-id={ id }
> >
<button <button
data-action="load-filter" data-action="load-filter"
@click="loadFilter($event)" @click="loadFilter($event)"
class="flex-1 text-left px-2 py-1 rounded" class="flex-1 text-left px-2 py-1 text-sm rounded-lg hover:bg-surface-hover"
style="color: var(--text-primary);" style="color: var(--text-primary);"
> >
{ name } { name }
@@ -18,11 +16,12 @@ templ FilterItem(id string, name string) {
<button <button
data-action="delete-filter" data-action="delete-filter"
@click="deleteFilter($event)" @click="deleteFilter($event)"
class="p-1 hover:opacity-70 rounded" class="icon-btn shrink-0"
style="color: var(--text-secondary);" style="color: var(--text-secondary);"
title="Delete filter" title="Delete filter"
aria-label="Delete filter"
> >
🗑 @Icon("trash", "h-4 w-4")
</button> </button>
</div> </div>
} }
+13 -6
View File
@@ -8,7 +8,6 @@ package templates
import "github.com/a-h/templ" import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime" import templruntime "github.com/a-h/templ/runtime"
// FilterItem renders a single saved filter item for the list
func FilterItem(id string, name string) templ.Component { func FilterItem(id string, name string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -30,33 +29,41 @@ func FilterItem(id string, name string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex items-center justify-between p-2 rounded hover:opacity-80\" style=\"background-color: var(--bg-primary);\" data-filter-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex items-center justify-between gap-2 p-2 rounded-lg hover:bg-surface-hover\" data-filter-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(id) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 8, Col: 21} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 6, Col: 21}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><button data-action=\"load-filter\" @click=\"loadFilter($event)\" class=\"flex-1 text-left px-2 py-1 rounded\" style=\"color: var(--text-primary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><button data-action=\"load-filter\" @click=\"loadFilter($event)\" class=\"flex-1 text-left px-2 py-1 text-sm rounded-lg hover:bg-surface-hover\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 16, Col: 9} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 14, Col: 9}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</button> <button data-action=\"delete-filter\" @click=\"deleteFilter($event)\" class=\"p-1 hover:opacity-70 rounded\" style=\"color: var(--text-secondary);\" title=\"Delete filter\">🗑</button></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</button> <button data-action=\"delete-filter\" @click=\"deleteFilter($event)\" class=\"icon-btn shrink-0\" style=\"color: var(--text-secondary);\" title=\"Delete filter\" aria-label=\"Delete filter\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</button></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+447 -43
View File
File diff suppressed because one or more lines are too long
+79 -4
View File
@@ -29,17 +29,92 @@ func Index(loggedIn bool) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</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\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><!-- Hero Section --><div class=\"relative overflow-hidden\"><div class=\"absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-700 opacity-10\"></div><div class=\"relative max-w-7xl mx-auto px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8\"><div class=\"flex justify-between items-start mb-8\"><div></div><select id=\"theme-select\" @change=\"changeTheme\" class=\"px-3 py-2 border rounded text-sm\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"tokyo-night\">Tokyo Night</option> <option value=\"dracula\">Dracula</option> <option value=\"nord\">Nord</option> <option value=\"solarized-dark\">Solarized Dark</option> <option value=\"monokai\">Monokai</option> <option value=\"one-dark-pro\">One Dark Pro</option> <option value=\"material-dark\">Material Dark</option> <option value=\"catppuccin-mocha\">Catppuccin Mocha</option> <option value=\"catppuccin-macchiato\">Catppuccin Macchiato</option> <option value=\"catppuccin-frappe\">Catppuccin Frappé</option> <option value=\"catppuccin-latte\">Catppuccin Latte</option></select></div><div class=\"text-center\"><h1 class=\"text-4xl sm:text-6xl lg:text-7xl font-extrabold\" style=\"color: var(--text-primary)\">📚 Bookhoard</h1><p class=\"mt-6 max-w-2xl mx-auto text-xl\" style=\"color: var(--text-secondary)\">Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes.</p><div class=\"mt-10\"><a href=\"#auth\" class=\"btn-primary px-8 py-3 rounded-lg font-medium text-lg\">Get Started</a></div></div></div></div><!-- Features Section --><div class=\"py-16\" style=\"background-color: var(--bg-secondary)\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold\" style=\"color: var(--text-primary)\">Why Bookhoard?</h2><p class=\"mt-4 text-lg\" style=\"color: var(--text-secondary)\">Discover the features that make ebook management effortless</p></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-8\"><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">📖</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Reading Progress</h3><p style=\"color: var(--text-secondary)\">Track your reading progress across all your ebooks with detailed statistics.</p></div></div><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-purple-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">🎨</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Beautiful Themes</h3><p style=\"color: var(--text-secondary)\">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p></div></div><div class=\"card p-6 rounded-xl border\"><div class=\"text-center\"><div class=\"mx-auto w-12 h-12 bg-green-500 rounded-lg flex items-center justify-center mb-4\"><span class=\"text-white text-2xl\">⚡</span></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Fast & Modern</h3><p style=\"color: var(--text-secondary)\">Built with Go and HTMX for lightning-fast performance and smooth interactions.</p></div></div></div></div></div><!-- Auth Section -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</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\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><div class=\"absolute top-4 right-4 z-10\"><select id=\"theme-select\" @change=\"changeTheme\" class=\"input w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if !loggedIn { for _, opt := range ThemeOptions {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"auth\" class=\"py-16\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold\" style=\"color: var(--text-primary)\">Join Bookhoard Today</h2><p class=\"mt-4 text-lg\" style=\"color: var(--text-secondary)\">Sign in to start managing your ebook library</p></div><div class=\"grid grid-cols-1 gap-8\"><div class=\"card p-8 rounded-xl border\"><h3 class=\"text-2xl font-semibold mb-6 text-center\" style=\"color: var(--text-primary)\">Login</h3><form hx-post=\"/api/auth/login\" hx-target=\"#auth-result\" hx-swap=\"innerHTML\"><div class=\"space-y-4\"><div><label class=\"block text-sm font-medium 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-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" class=\"w-full btn-primary py-2 rounded-lg font-medium\">Sign In</button></div></form></div></div><div id=\"auth-result\" class=\"mt-8 text-center\"></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 23, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 23, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</option>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</select></div><!-- Hero --><div class=\"brand-gradient relative overflow-hidden\"><div class=\"max-w-7xl mx-auto px-4 py-20 sm:px-6 sm:py-28 lg:py-36 lg:px-8 text-center\"><div class=\"inline-flex items-center gap-3 mb-6\"><span class=\"text-5xl\">📚</span> <span class=\"text-4xl sm:text-6xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></div><p class=\"mt-4 max-w-2xl mx-auto text-lg sm:text-xl\" style=\"color: var(--text-secondary)\">Your personal ebook management system. Track reading progress, organize your library, and sync across every device.</p><div class=\"mt-10 flex items-center justify-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if loggedIn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/dashboard\" class=\"btn btn-primary px-8 py-3 text-base\">Go to Library</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"#auth\" class=\"btn btn-primary px-8 py-3 text-base\">Get Started</a> <a href=\"#features\" class=\"btn btn-secondary px-6 py-3 text-base\">Learn more</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div><!-- Features --><div id=\"features\" class=\"py-16 sm:py-20\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"text-center mb-12\"><h2 class=\"text-3xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Why Bookhoard?</h2><p class=\"mt-3 text-lg\" style=\"color: var(--text-secondary)\">Everything you need to manage your ebook library</p></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-6\"><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Reading Progress</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Track progress across all your ebooks with detailed statistics and sync.</p></div><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("palette", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Beautiful Themes</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin.</p></div><div class=\"card p-6\"><div class=\"grid place-items-center h-12 w-12 rounded-xl mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("device", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">Cross-Device Sync</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Built with Go and HTMX for fast performance and seamless device syncing.</p></div></div></div></div><!-- Auth -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !loggedIn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div id=\"auth\" class=\"pb-20 px-4\"><div class=\"max-w-md mx-auto\"><div class=\"text-center mb-8\"><h2 class=\"text-2xl font-extrabold tracking-tight\" style=\"color: var(--text-primary)\">Welcome back</h2><p class=\"mt-2\" style=\"color: var(--text-secondary)\">Sign in to start managing your ebook library</p></div><div class=\"card p-8\"><form hx-post=\"/api/auth/login\" hx-target=\"#auth-result\" hx-swap=\"innerHTML\" class=\"space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full py-2.5\">Sign In</button></form><p class=\"text-center text-sm mt-4\" style=\"color: var(--text-secondary)\">No account? <a href=\"/register\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Register</a></p></div><div id=\"auth-result\" class=\"mt-4 text-center text-sm\"></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+25 -9
View File
@@ -29,14 +29,14 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b\" style=\"background-color: var(--bg-primary);\"><div class=\"w-full px-4 py-3 flex items-center justify-between\"><div class=\"flex items-center gap-4\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Library:</label> <select id=\"library-select\" name=\"library_id\" class=\"px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500\" style=\"background-color: var(--bg-secondary); color: var(--text-primary);\"><option value=\"\">All Libraries (") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"app-subbar backdrop-blur border-b\" style=\"background-color: color-mix(in srgb, var(--bg-primary) 88%, transparent); border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);\"><div class=\"w-full px-4 sm:px-6 lg:px-8 py-3 flex items-center justify-between gap-4\"><div class=\"flex items-center gap-3\"><label class=\"text-sm font-medium\" style=\"color: var(--text-secondary)\">Library</label> <select id=\"library-select\" name=\"library_id\" class=\"input w-auto py-1.5 pr-8 cursor-pointer\"><option value=\"\">All Libraries (")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData)) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 14, Col: 62} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 63}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -55,7 +55,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -68,7 +68,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 50} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 51}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -81,7 +81,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 17, Col: 70} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 71}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -99,7 +99,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -112,7 +112,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -125,7 +125,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 19, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 21, Col: 62}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -186,7 +186,23 @@ func DashboardActions() templ.Component {
templ_7745c5c3_Var9 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button data-action=\"open-dashboard-settings\" class=\"p-2 rounded-lg hover:bg-gray-700 transition-colors\" style=\"background-color: var(--bg-secondary);\" title=\"Customize Dashboard\">⚙️</button> <button data-action=\"reload-page\" class=\"p-2 rounded-lg hover:bg-gray-700 transition-colors\" style=\"background-color: var(--bg-secondary);\" title=\"Refresh\">🔄</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<button data-action=\"open-dashboard-settings\" class=\"icon-btn\" title=\"Customize Dashboard\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("settings", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button> <button data-action=\"reload-page\" class=\"icon-btn\" title=\"Refresh\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+56 -4
View File
@@ -29,23 +29,75 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login</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=\"login\" x-init=\"initLoginTheme()\"><div class=\"container mx-auto px-4 py-8 max-w-md\"><div class=\"flex justify-between items-center mb-8\"><h1 class=\"text-3xl font-bold\">Login to Bookhoard</h1><select id=\"theme-select\" @change=\"changeTheme()\" class=\"px-3 py-2 border rounded\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)\"><option value=\"tokyo-night\">Tokyo Night</option> <option value=\"dracula\">Dracula</option> <option value=\"nord\">Nord</option> <option value=\"solarized-dark\">Solarized Dark</option> <option value=\"monokai\">Monokai</option> <option value=\"one-dark-pro\">One Dark Pro</option> <option value=\"material-dark\">Material Dark</option> <option value=\"catppuccin-mocha\">Catppuccin Mocha</option> <option value=\"catppuccin-macchiato\">Catppuccin Macchiato</option> <option value=\"catppuccin-frappe\">Catppuccin Frappé</option> <option value=\"catppuccin-latte\">Catppuccin Latte</option></select></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login - 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=\"login\" x-init=\"initLoginTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative\"><select id=\"theme-select\" @change=\"changeTheme()\" class=\"input absolute top-4 right-4 w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, opt := range ThemeOptions {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</select><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if sessionExpired { if sessionExpired {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-4 p-3 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--accent);\"><p style=\"color: var(--text-primary);\">Your session has expired. Please log in again to continue.</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-4 p-3 rounded-xl flex items-start gap-2\" style=\"background-color: color-mix(in srgb, var(--accent) 14%, transparent); border: 1px solid var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("info", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-primary)\">Your session has expired. Please log in again to continue.</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if deleted { if deleted {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"mb-4 p-3 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--accent);\"><p style=\"color: var(--text-primary);\">Your account has been deleted successfully.</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-4 p-3 rounded-xl flex items-start gap-2\" style=\"background-color: color-mix(in srgb, var(--status-success) 14%, transparent); border: 1px solid var(--status-success);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"text-sm\" style=\"color: var(--text-primary)\">Your account has been deleted successfully.</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form hx-post=\"/api/auth/login\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 rounded-lg shadow-md border\"><div class=\"mb-4\"><label class=\"block mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" class=\"btn-primary w-full py-2 rounded\">Sign In</button></form><div id=\"result\" class=\"mt-4 text-center\"></div><p class=\"text-center mt-4\"><a href=\"/register\" class=\"text-blue-500 hover:underline\">Don't have an account? Sign Up</a></p></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<form hx-post=\"/api/auth/login\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full py-2.5\">Sign In</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Don't have an account? <a href=\"/register\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Sign up</a></p></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+23 -12
View File
@@ -11,21 +11,32 @@ templ Profile(user User) {
</head> </head>
<body class="theme-{ user.Theme }" x-data="profile"> <body class="theme-{ user.Theme }" x-data="profile">
@Header(user, "/profile") @Header(user, "/profile")
<main class="max-w-4xl mx-auto px-4 py-8"> <main class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8 flex items-center gap-3">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Profile Settings</h1> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
<p style="color: var(--text-secondary)">Manage your account information and preferences</p> @Icon("user", "h-5 w-5")
</span>
<div>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Profile Settings</h1>
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Manage your account information and preferences</p>
</div>
</div> </div>
<div class="space-y-8"> <div class="space-y-6">
<!-- Account Information & Password - uses reusable ProfileForm --> <div class="card p-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
@ProfileForm(user, "/api/auth/profile", true, false, false) @ProfileForm(user, "/api/auth/profile", true, false, false)
</div> </div>
<!-- Danger Zone --> <div class="card p-6">
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="flex items-start gap-3 mb-4">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Danger Zone</h3> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: color-mix(in srgb, var(--status-danger) 18%, transparent); color: var(--status-danger);">
<p style="color: var(--text-secondary)" class="mb-4">Once you delete your account, there is no going back. Please be certain.</p> @Icon("alert", "h-5 w-5")
<button @click="confirmDeleteAccount()" class="px-4 py-2 rounded text-sm" style="background-color: #dc2626; color: white;"> </span>
<div>
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Danger Zone</h3>
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Once you delete your account, there is no going back. Please be certain.</p>
</div>
</div>
<button @click="confirmDeleteAccount()" class="btn btn-danger">
@Icon("trash", "h-4 w-4")
Remove My Account Remove My Account
</button> </button>
</div> </div>
+40 -52
View File
@@ -6,60 +6,57 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
hx-target="#profile-result" hx-target="#profile-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
class="space-y-6" class="space-y-8"
> >
<!-- Account Information -->
<div> <div>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Account Information</h3> <div class="flex items-center gap-2 mb-4">
@Icon("user", "h-5 w-5")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Account Information</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Username</label>
<input <input
type="text" type="text"
name="username" name="username"
value={ user.Username } value={ user.Username }
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email Address</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Email Address</label>
<input <input
type="email" type="email"
name="email" name="email"
value={ user.Email } value={ user.Email }
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">First Name</label>
<input <input
type="text" type="text"
name="first_name" name="first_name"
value={ user.FirstName } value={ user.FirstName }
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Last Name</label>
<input <input
type="text" type="text"
name="last_name" name="last_name"
value={ user.LastName } value={ user.LastName }
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Theme</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Theme</label>
<select <select
name="theme" name="theme"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="tokyo-night" selected?={ user.Theme == "tokyo-night" }>Tokyo Night</option> <option value="tokyo-night" selected?={ user.Theme == "tokyo-night" }>Tokyo Night</option>
<option value="dracula" selected?={ user.Theme == "dracula" }>Dracula</option> <option value="dracula" selected?={ user.Theme == "dracula" }>Dracula</option>
@@ -71,11 +68,10 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Timezone</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Timezone</label>
<select <select
name="timezone" name="timezone"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="UTC" selected?={ user.Timezone == "UTC" }>UTC (UTC+0)</option> <option value="UTC" selected?={ user.Timezone == "UTC" }>UTC (UTC+0)</option>
<option value="Pacific/Honolulu" selected?={ user.Timezone == "Pacific/Honolulu" }>Hawaii (UTC-10)</option> <option value="Pacific/Honolulu" selected?={ user.Timezone == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
@@ -105,11 +101,10 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
</div> </div>
if showRoleField { if showRoleField {
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Role</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Role</label>
<select <select
name="role" name="role"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="user" selected?={ user.Role == "user" }>User</option> <option value="user" selected?={ user.Role == "user" }>User</option>
<option value="admin" selected?={ user.Role == "admin" }>Admin</option> <option value="admin" selected?={ user.Role == "admin" }>Admin</option>
@@ -118,39 +113,37 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
} }
</div> </div>
</div> </div>
<!-- Change Password Section -->
<div> <div>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Change Password</h3> <div class="flex items-center gap-2 mb-4">
@Icon("shield", "h-5 w-5")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Change Password</h3>
</div>
if requireCurrentPassword { if requireCurrentPassword {
<!-- Self-edit: require current password -->
<div class="space-y-4 max-w-md"> <div class="space-y-4 max-w-md">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Password</label>
<input <input
type="password" type="password"
name="current_password" name="current_password"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
required required
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
<input <input
type="password" type="password"
name="new_password" name="new_password"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
minlength="6" minlength="6"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
<input <input
type="password" type="password"
name="confirm_password" name="confirm_password"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
minlength="6" minlength="6"
/> />
</div> </div>
@@ -161,36 +154,33 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
hx-target="#password-result" hx-target="#password-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="closest form" hx-include="closest form"
class="px-4 py-2 rounded" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
@Icon("save", "h-4 w-4")
Update Password Update Password
</button> </button>
<div id="password-result" class="mt-2"></div> <div id="password-result" class="mt-2"></div>
</div> </div>
} else { } else {
<!-- Admin edit: no current password required -->
<p class="text-sm italic mb-4" style="color: var(--text-secondary);"> <p class="text-sm italic mb-4" style="color: var(--text-secondary);">
As an admin, you can change this user's password without knowing their current password. As an admin, you can change this user's password without knowing their current password.
</p> </p>
<div class="space-y-4 max-w-md"> <div class="space-y-4 max-w-md">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">New Password</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
<input <input
type="password" type="password"
name="new_password" name="new_password"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
minlength="6" minlength="6"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm New Password</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
<input <input
type="password" type="password"
name="confirm_password" name="confirm_password"
class="w-full px-3 py-2 border rounded" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
minlength="6" minlength="6"
/> />
</div> </div>
@@ -201,32 +191,30 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
hx-target="#password-result" hx-target="#password-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="closest form" hx-include="closest form"
class="px-4 py-2 rounded" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
@Icon("save", "h-4 w-4")
Reset Password Reset Password
</button> </button>
<div id="password-result" class="mt-2"></div> <div id="password-result" class="mt-2"></div>
</div> </div>
} }
</div> </div>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-6 border-t" style="border-color: var(--border);"> <div class="flex justify-end gap-3 pt-6 border-t" style="border-color: var(--border);">
if showCancelButton { if showCancelButton {
<button <button
type="button" type="button"
@click="closeProfileModal()" @click="closeProfileModal()"
class="px-4 py-2 rounded" class="btn btn-secondary"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
> >
Cancel Cancel
</button> </button>
} }
<button <button
type="submit" type="submit"
class="px-4 py-2 rounded" class="btn btn-primary"
style="background-color: var(--accent); color: white;"
> >
@Icon("save", "h-4 w-4")
Save Changes Save Changes
</button> </button>
</div> </div>
+123 -83
View File
@@ -42,33 +42,41 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-target=\"#profile-result\" hx-swap=\"innerHTML\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' class=\"space-y-6\"><!-- Account Information --><div><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Account Information</h3><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" name=\"username\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-target=\"#profile-result\" hx-swap=\"innerHTML\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' class=\"space-y-8\"><div><div class=\"flex items-center gap-2 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("user", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Account Information</h3></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" name=\"username\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Username) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 20, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 22, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email Address</label> <input type=\"email\" name=\"email\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Email Address</label> <input type=\"email\" name=\"email\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Email) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Email)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 30, Col: 24} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 31, Col: 24}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -81,398 +89,430 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.LastName) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.LastName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 51, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 50, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Theme</label> <select name=\"theme\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"tokyo-night\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Theme</label> <select name=\"theme\" class=\"input\"><option value=\"tokyo-night\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "tokyo-night" { if user.Theme == "tokyo-night" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, ">Tokyo Night</option> <option value=\"dracula\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">Tokyo Night</option> <option value=\"dracula\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "dracula" { if user.Theme == "dracula" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">Dracula</option> <option value=\"nord\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">Dracula</option> <option value=\"nord\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "nord" { if user.Theme == "nord" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">Nord</option> <option value=\"solarized-dark\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">Nord</option> <option value=\"solarized-dark\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "solarized-dark" { if user.Theme == "solarized-dark" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">Solarized Dark</option> <option value=\"monokai\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Solarized Dark</option> <option value=\"monokai\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "monokai" { if user.Theme == "monokai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">Monokai</option> <option value=\"one-dark-pro\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Monokai</option> <option value=\"one-dark-pro\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "one-dark-pro" { if user.Theme == "one-dark-pro" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">One Dark Pro</option> <option value=\"material-dark\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">One Dark Pro</option> <option value=\"material-dark\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Theme == "material-dark" { if user.Theme == "material-dark" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, ">Material Dark</option></select></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Timezone</label> <select name=\"timezone\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"UTC\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Material Dark</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Timezone</label> <select name=\"timezone\" class=\"input\"><option value=\"UTC\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "UTC" { if user.Timezone == "UTC" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Pacific/Honolulu" { if user.Timezone == "Pacific/Honolulu" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Anchorage" { if user.Timezone == "America/Anchorage" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Los_Angeles" { if user.Timezone == "America/Los_Angeles" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Denver" { if user.Timezone == "America/Denver" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Phoenix" { if user.Timezone == "America/Phoenix" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Chicago" { if user.Timezone == "America/Chicago" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/New_York" { if user.Timezone == "America/New_York" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "America/Sao_Paulo" { if user.Timezone == "America/Sao_Paulo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Europe/London" { if user.Timezone == "Europe/London" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Europe/Paris" { if user.Timezone == "Europe/Paris" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Europe/Helsinki" { if user.Timezone == "Europe/Helsinki" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Europe/Moscow" { if user.Timezone == "Europe/Moscow" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Tehran" { if user.Timezone == "Asia/Tehran" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Dubai" { if user.Timezone == "Asia/Dubai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Karachi" { if user.Timezone == "Asia/Karachi" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Kolkata" { if user.Timezone == "Asia/Kolkata" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Dhaka" { if user.Timezone == "Asia/Dhaka" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Bangkok" { if user.Timezone == "Asia/Bangkok" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Shanghai" { if user.Timezone == "Asia/Shanghai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Asia/Tokyo" { if user.Timezone == "Asia/Tokyo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Australia/Darwin" { if user.Timezone == "Australia/Darwin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Australia/Sydney" { if user.Timezone == "Australia/Sydney" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Timezone == "Pacific/Auckland" { if user.Timezone == "Pacific/Auckland" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, ">New Zealand (UTC+12/+13)</option></select></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, ">New Zealand (UTC+12/+13)</option></select></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if showRoleField { if showRoleField {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Role</label> <select name=\"role\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"user\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Role</label> <select name=\"role\" class=\"input\"><option value=\"user\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Role == "user" { if user.Role == "user" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, ">User</option> <option value=\"admin\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, ">User</option> <option value=\"admin\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if user.Role == "admin" { if user.Role == "admin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, ">Admin</option></select></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, ">Admin</option></select></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</div></div><!-- Change Password Section --><div><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Change Password</h3>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</div></div><div><div class=\"flex items-center gap-2 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("shield", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Change Password</h3></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if requireCurrentPassword { if requireCurrentPassword {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<!-- Self-edit: require current password --> <div class=\"space-y-4 max-w-md\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Current Password</label> <input type=\"password\" name=\"current_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" required></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><button type=\"button\" hx-put=\"/api/auth/password\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Update Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<div class=\"space-y-4 max-w-md\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Current Password</label> <input type=\"password\" name=\"current_password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"input\" minlength=\"6\"></div><button type=\"button\" hx-put=\"/api/auth/password\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "Update Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<!-- Admin edit: no current password required --> <p class=\"text-sm italic mb-4\" style=\"color: var(--text-secondary);\">As an admin, you can change this user's password without knowing their current password.</p><div class=\"space-y-4 max-w-md\"><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><div><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" minlength=\"6\"></div><button type=\"button\" hx-put=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<p class=\"text-sm italic mb-4\" style=\"color: var(--text-secondary);\">As an admin, you can change this user's password without knowing their current password.</p><div class=\"space-y-4 max-w-md\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Confirm New Password</label> <input type=\"password\" name=\"confirm_password\" class=\"input\" minlength=\"6\"></div><button type=\"button\" hx-put=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/password/" + user.ID) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/password/" + user.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 199, Col: 46} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 189, Col: 46}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Reset Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "Reset Password</button><div id=\"password-result\" class=\"mt-2\"></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</div><!-- Actions --><div class=\"flex justify-end gap-3 pt-6 border-t\" style=\"border-color: var(--border);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</div><div class=\"flex justify-end gap-3 pt-6 border-t\" style=\"border-color: var(--border);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if showCancelButton { if showCancelButton {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<button type=\"button\" @click=\"closeProfileModal()\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);\">Cancel</button> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<button type=\"button\" @click=\"closeProfileModal()\" class=\"btn btn-secondary\">Cancel</button> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<button type=\"submit\" class=\"px-4 py-2 rounded\" style=\"background-color: var(--accent); color: white;\">Save Changes</button></div><div id=\"profile-result\" class=\"mt-2\"></div></form>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "<button type=\"submit\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "Save Changes</button></div><div id=\"profile-result\" class=\"mt-2\"></div></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+16 -14
View File
@@ -1,26 +1,28 @@
package templates package templates
templ ProfileModal(user User) { templ ProfileModal(user User) {
<div id="profile-modal" class="fixed inset-0 flex items-center justify-center z-50" x-data="profileModal" x-init="setupProfileModal()" style="background-color: rgba(0,0,0,0.5);"> <div id="profile-modal" class="fixed inset-0 flex items-center justify-center z-50 p-4" x-data="profileModal" x-init="setupProfileModal()" style="background-color: var(--surface-overlay);">
<div class="rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary);"> <div class="card w-full max-w-4xl max-h-[90vh] overflow-y-auto" style="box-shadow: var(--shadow-pop);">
<!-- Modal Header --> <div class="flex justify-between items-start gap-4 p-6 border-b" style="border-color: var(--border);">
<div class="flex justify-between items-center p-6 border-b" style="border-color: var(--border);"> <div class="flex items-center gap-3">
<div> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit User Profile</h2> @Icon("edit", "h-5 w-5")
<p class="text-sm mt-1" style="color: var(--text-secondary);"> </span>
{ user.Username } ({ user.Email }) <div>
</p> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit User Profile</h2>
<p class="text-sm mt-0.5" style="color: var(--text-secondary);">
{ user.Username } ({ user.Email })
</p>
</div>
</div> </div>
<button <button
@click="closeProfileModal()" @click="closeProfileModal()"
class="text-2xl" class="icon-btn shrink-0"
style="color: var(--text-secondary);" aria-label="Close"
> >
&times; @Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
<!-- Modal Body - uses reusable form -->
<div class="p-6"> <div class="p-6">
@ProfileForm(user, "/api/auth/profile/" + user.ID, false, true, true) @ProfileForm(user, "/api/auth/profile/" + user.ID, false, true, true)
</div> </div>
+22 -6
View File
@@ -29,33 +29,49 @@ func ProfileModal(user User) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"profile-modal\" class=\"fixed inset-0 flex items-center justify-center z-50\" x-data=\"profileModal\" x-init=\"setupProfileModal()\" style=\"background-color: rgba(0,0,0,0.5);\"><div class=\"rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto\" style=\"background-color: var(--bg-secondary);\"><!-- Modal Header --><div class=\"flex justify-between items-center p-6 border-b\" style=\"border-color: var(--border);\"><div><h2 class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">Edit User Profile</h2><p class=\"text-sm mt-1\" style=\"color: var(--text-secondary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"profile-modal\" class=\"fixed inset-0 flex items-center justify-center z-50 p-4\" x-data=\"profileModal\" x-init=\"setupProfileModal()\" style=\"background-color: var(--surface-overlay);\"><div class=\"card w-full max-w-4xl max-h-[90vh] overflow-y-auto\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-start gap-4 p-6 border-b\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><div><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit User Profile</h2><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 11, Col: 21} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " (") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " (")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 11, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 38}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ")</p></div><button @click=\"closeProfileModal()\" class=\"text-2xl\" style=\"color: var(--text-secondary);\">&times;</button></div><!-- Modal Body - uses reusable form --><div class=\"p-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, ")</p></div></div><button @click=\"closeProfileModal()\" class=\"icon-btn shrink-0\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</button></div><div class=\"p-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -63,7 +79,7 @@ func ProfileModal(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+26 -2
View File
@@ -37,7 +37,15 @@ func Profile(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"max-w-4xl mx-auto px-4 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold mb-2\" style=\"color: var(--text-primary)\">Profile Settings</h1><p style=\"color: var(--text-secondary)\">Manage your account information and preferences</p></div><div class=\"space-y-8\"><!-- Account Information & Password - uses reusable ProfileForm --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("user", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Profile Settings</h1><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Manage your account information and preferences</p></div></div><div class=\"space-y-6\"><div class=\"card p-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,7 +53,23 @@ func Profile(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><!-- Danger Zone --><div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Danger Zone</h3><p style=\"color: var(--text-secondary)\" class=\"mb-4\">Once you delete your account, there is no going back. Please be certain.</p><button @click=\"confirmDeleteAccount()\" class=\"px-4 py-2 rounded text-sm\" style=\"background-color: #dc2626; color: white;\">Remove My Account</button></div></div></main></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"card p-6\"><div class=\"flex items-start gap-3 mb-4\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: color-mix(in srgb, var(--status-danger) 18%, transparent); color: var(--status-danger);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("alert", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Danger Zone</h3><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Once you delete your account, there is no going back. Please be certain.</p></div></div><button @click=\"confirmDeleteAccount()\" class=\"btn btn-danger\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("trash", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Remove My Account</button></div></div></main></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+48 -44
View File
@@ -16,90 +16,94 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
<body class="theme-{ user.Theme }"> <body class="theme-{ user.Theme }">
@Header(user, "/progress") @Header(user, "/progress")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="w-full px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8 flex items-center gap-3">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Reading Progress</h1> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
<p style="color: var(--text-secondary)">Track your reading progress across all devices</p> @Icon("book-open", "h-5 w-5")
</span>
<div>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Reading Progress</h1>
<p class="text-sm mt-0.5" style="color: var(--text-secondary)">Track your reading progress across all devices</p>
</div>
</div> </div>
<div id="progress-container" class="space-y-6"> <div id="progress-container" class="space-y-6">
if len(progressData) == 0 { if len(progressData) == 0 {
<div class="text-center py-16" style="color: var(--text-secondary)"> <div class="card text-center py-16 px-6" style="color: var(--text-secondary)">
<div class="text-6xl mb-4">📖</div> <div class="flex justify-center mb-4">
@Icon("book-open", "h-12 w-12 opacity-50")
</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Progress Yet</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Progress Yet</h3>
<p>Start reading a book to track your progress</p> <p>Start reading a book to track your progress</p>
</div> </div>
} }
for _, item := range progressData { for _, item := range progressData {
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6">
<div class="flex gap-6"> <div class="flex gap-6">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img <img
src="{ item.CoverImagePath }" src={ item.CoverImagePath }
alt="Cover" alt="Cover"
class="w-24 h-32 object-cover rounded" class="w-24 h-32 object-cover rounded-lg"
onerror="this.src='/static/placeholder-book.svg'" onerror="this.src='/static/placeholder-book.svg'"
/> />
</div> </div>
<div class="flex-1"> <div class="flex-1 min-w-0">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start gap-4 mb-3">
<div> <div class="min-w-0">
<a <a
href={ "/media/" + item.MediaItemID.String() } href={ "/media/" + item.MediaItemID.String() }
class="inline-block hover:underline"
style="color: var(--text-primary); text-decoration: none;" style="color: var(--text-primary); text-decoration: none;"
> >
<h3 class="font-semibold text-lg hover:opacity-80">{ item.Title }</h3> <h3 class="font-semibold text-lg">{ item.Title }</h3>
</a> </a>
if item.Author != "" { if item.Author != "" {
<p class="text-sm" style="color: var(--text-secondary)">by { item.Author }</p> <p class="text-sm mt-0.5" style="color: var(--text-secondary)">by { item.Author }</p>
} }
</div> </div>
<div class="text-right"> <span class="text-2xl font-bold shrink-0" style="color: var(--accent)">{ item.ProgressPercentage }%</span>
<span class="text-2xl font-bold" style="color: var(--accent)">{ item.ProgressPercentage }%</span>
</div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<div class="w-full bg-gray-700 rounded-full h-3"> <div class="w-full rounded-full h-2.5" style="background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);">
<div class="h-3 rounded-full transition-all" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);" }></div> <div class="h-2.5 rounded-full transition-all" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);" }></div>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div> <div class="stat-card">
<p style="color: var(--text-secondary)">Current Position</p> <span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Position</span>
<p style="color: var(--text-primary)"> <p class="text-sm font-medium" style="color: var(--text-primary)">
if item.FormatGroup == "reflowable" { if item.FormatGroup == "reflowable" {
if item.EstimatedPages > 0 { if item.EstimatedPages > 0 {
{ fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages) } { fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages) }
} else {
{ fmt.Sprintf("%.1f%%", item.ProgressPercentage) }
}
} else { } else {
{ fmt.Sprintf("%.1f%%", item.ProgressPercentage) } { fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages) }
} }
} else { </p>
{ fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages) }
}
</p>
</div> </div>
<div> <div class="stat-card">
<p style="color: var(--text-secondary)">Last Updated</p> <span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Last Updated</span>
<p style="color: var(--text-primary)">{ item.LastUpdated }</p> <p class="text-sm font-medium" style="color: var(--text-primary)">{ item.LastUpdated }</p>
</div> </div>
<div> <div class="stat-card">
<p style="color: var(--text-secondary)">Synced From</p> <span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Synced From</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xl">{ item.DeviceIcon }</span> <span class="text-xl">{ item.DeviceIcon }</span>
<span style="color: var(--text-primary)">{ item.DeviceName }</span> <span class="text-sm font-medium" style="color: var(--text-primary)">{ item.DeviceName }</span>
</div> </div>
</div> </div>
</div> </div>
if item.DeviceIcon != "" { if item.DeviceIcon != "" {
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);"> <div class="mt-3 pt-3 border-t flex items-center gap-2 text-xs" style="border-color: var(--border); color: var(--text-secondary);">
<div class="flex items-center gap-2 text-xs" style="color: var(--text-secondary);"> @Icon("sync", "h-4 w-4")
<span>🔄</span> <span>Last sync from <strong style="color: var(--text-primary)">{ item.DeviceName }</strong></span>
<span>Last sync from <strong>{ item.DeviceName }</strong></span> <span>({ item.DeviceType })</span>
<span>({ item.DeviceType })</span>
</div>
</div> </div>
} }
if item.EpubCFI != "" { if item.EpubCFI != "" {
<div class="mt-2 text-xs" style="color: var(--text-secondary);"> <div class="mt-2 flex items-center gap-2 text-xs" style="color: var(--text-secondary);">
<span>📍</span> @Icon("bookmark", "h-3.5 w-3.5")
<span>CFI: { item.EpubCFI }</span> <span>CFI: { item.EpubCFI }</span>
</div> </div>
} }
+139 -94
View File
@@ -40,229 +40,274 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Reading Progress</h1><p style=\"color: var(--text-secondary)\">Track your reading progress across all devices</p></div><div id=\"progress-container\" class=\"space-y-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Reading Progress</h1><p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">Track your reading progress across all devices</p></div></div><div id=\"progress-container\" class=\"space-y-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(progressData) == 0 { if len(progressData) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📖</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Progress Yet</h3><p>Start reading a book to track your progress</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card text-center py-16 px-6\" style=\"color: var(--text-secondary)\"><div class=\"flex justify-center mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-12 w-12 opacity-50").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Progress Yet</h3><p>Start reading a book to track your progress</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, item := range progressData { for _, item := range progressData {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-6\"><div class=\"flex-shrink-0\"><img src=\"{ item.CoverImagePath }\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1\"><div class=\"flex justify-between items-start mb-2\"><div><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6\"><div class=\"flex gap-6\"><div class=\"flex-shrink-0\"><img src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 templ.SafeURL var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String()) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 46, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 43, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" style=\"color: var(--text-primary); text-decoration: none;\"><h3 class=\"font-semibold text-lg hover:opacity-80\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded-lg\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex justify-between items-start gap-4 mb-3\"><div class=\"min-w-0\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 49, Col: 75} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 53, Col: 56}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h3></a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"inline-block hover:underline\" style=\"color: var(--text-primary); text-decoration: none;\"><h3 class=\"font-semibold text-lg\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 57, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h3></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.Author != "" { if item.Author != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">by ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<p class=\"text-sm mt-0.5\" style=\"color: var(--text-secondary)\">by ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 52, Col: 84} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 60, Col: 91}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><div class=\"text-right\"><span class=\"text-2xl font-bold\" style=\"color: var(--accent)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><span class=\"text-2xl font-bold shrink-0\" style=\"color: var(--accent)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.ProgressPercentage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 56, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "%</span></div></div><div class=\"mb-4\"><div class=\"w-full bg-gray-700 rounded-full h-3\"><div class=\"h-3 rounded-full transition-all\" style=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);") templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.ProgressPercentage)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 61, Col: 160} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 63, Col: 106}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 text-sm\"><div><p style=\"color: var(--text-secondary)\">Current Position</p><p style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "%</span></div><div class=\"mb-4\"><div class=\"w-full rounded-full h-2.5\" style=\"background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);\"><div class=\"h-2.5 rounded-full transition-all\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 67, Col: 162}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-3\"><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Current Position</span><p class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.FormatGroup == "reflowable" { if item.FormatGroup == "reflowable" {
if item.EstimatedPages > 0 { if item.EstimatedPages > 0 {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 70, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%%", item.ProgressPercentage)) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 72, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 76, Col: 90}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%%", item.ProgressPercentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 78, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} }
} else { } else {
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages)) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 75, Col: 71} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 81, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div><div><p style=\"color: var(--text-secondary)\">Last Updated</p><p style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Last Updated</span><p class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 81, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</p></div><div><p style=\"color: var(--text-secondary)\">Synced From</p><div class=\"flex items-center gap-2\"><span class=\"text-xl\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 86, Col: 51} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 87, Col: 95}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div><div class=\"stat-card\"><span class=\"block text-xs font-semibold uppercase tracking-wide mb-1.5\" style=\"color: var(--text-secondary)\">Synced From</span><div class=\"flex items-center gap-2\"><span class=\"text-xl\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 87, Col: 70} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 92, Col: 51}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span> <span class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 93, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.DeviceIcon != "" { if item.DeviceIcon != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"mt-3 pt-3 border-t\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-2 text-xs\" style=\"color: var(--text-secondary);\"><span>🔄</span> <span>Last sync from <strong>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mt-3 pt-3 border-t flex items-center gap-2 text-xs\" style=\"border-color: var(--border); color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string templ_7745c5c3_Err = Icon("sync", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 95, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</strong></span> <span>(") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span>Last sync from <strong style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 96, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 100, Col: 92}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ")</span></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</strong></span> <span>(")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if item.EpubCFI != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mt-2 text-xs\" style=\"color: var(--text-secondary);\"><span>📍</span> <span>CFI: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 103, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 101, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ")</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></div></div>") if item.EpubCFI != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mt-2 flex items-center gap-2 text-xs\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("bookmark", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>CFI: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 107, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -270,7 +315,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+63 -55
View File
@@ -14,53 +14,54 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
</head> </head>
<body x-data="queue" class="theme-{ user.Theme }"> <body x-data="queue" class="theme-{ user.Theme }">
@Header(user, "/queue") @Header(user, "/queue")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center gap-4 flex-wrap">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Queue</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Monitor and manage offline sync queue</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("clock", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Sync Queue</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Monitor and manage offline sync queue</p>
</div> </div>
<div class="flex space-x-3"> <div class="flex space-x-3">
<button @click="processPendingItems()" class="btn-primary px-4 py-2 rounded-lg"> <button @click="processPendingItems()" class="btn btn-primary">
@Icon("play", "h-4 w-4")
Process Queue Process Queue
</button> </button>
<button @click="clearFailedItems()" class="btn-secondary px-4 py-2 rounded-lg"> <button @click="clearFailedItems()" class="btn btn-secondary">Clear Failed</button>
Clear Failed <button @click="clearAllItems()" class="btn btn-danger">Clear All</button>
</button>
<button @click="clearAllItems()" class="btn-danger px-4 py-2 rounded-lg">
Clear All
</button>
</div> </div>
</div> </div>
<div id="stats-bar" class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4"> <div id="stats-bar" class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card text-center">
<div class="text-3xl font-bold" style="color: #f59e0b;">{ stats.PendingCount }</div> <div class="text-3xl font-bold" style="color: var(--status-warning);">{ stats.PendingCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Pending</div> <div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Pending</div>
</div> </div>
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card text-center">
<div class="text-3xl font-bold" style="color: #3b82f6;">{ stats.ProcessingCount }</div> <div class="text-3xl font-bold" style="color: var(--status-info);">{ stats.ProcessingCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Processing</div> <div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Processing</div>
</div> </div>
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card text-center">
<div class="text-3xl font-bold" style="color: #10b981;">{ stats.CompletedCount }</div> <div class="text-3xl font-bold" style="color: var(--status-success);">{ stats.CompletedCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Completed</div> <div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Completed</div>
</div> </div>
<div class="card p-4 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="stat-card text-center">
<div class="text-3xl font-bold" style="color: #ef4444;">{ stats.FailedCount }</div> <div class="text-3xl font-bold" style="color: var(--status-danger);">{ stats.FailedCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Failed</div> <div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Failed</div>
</div> </div>
</div> </div>
</div> </div>
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-4 items-end">
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Status Filter</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Status Filter</label>
<select <select
id="status-filter" id="status-filter"
onchange="filterQueue()" onchange="filterQueue()"
class="px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="all">All Items</option> <option value="all">All Items</option>
<option value="pending" selected>Pending</option> <option value="pending" selected>Pending</option>
@@ -70,12 +71,11 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Type</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Sync Type</label>
<select <select
id="type-filter" id="type-filter"
onchange="filterQueue()" onchange="filterQueue()"
class="px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="all">All Types</option> <option value="all">All Types</option>
<option value="progress">Progress</option> <option value="progress">Progress</option>
@@ -85,18 +85,18 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device</label> <label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Device</label>
<select <select
id="device-filter" id="device-filter"
onchange="filterQueue()" onchange="filterQueue()"
class="px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
> >
<option value="all">All Devices</option> <option value="all">All Devices</option>
</select> </select>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>
<button @click="refreshQueue()" class="btn-secondary px-4 py-2 rounded-lg"> <button @click="refreshQueue()" class="btn btn-secondary">
@Icon("refresh", "h-4 w-4")
Refresh Refresh
</button> </button>
</div> </div>
@@ -105,27 +105,31 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
<div class="loading-spinner mx-auto mb-4"></div> <div class="loading-spinner mx-auto mb-4"></div>
<p>Loading queue...</p> <p>Loading queue...</p>
</div> </div>
<div id="empty-state" class="hidden text-center py-16" style="color: var(--text-secondary)"> <div id="empty-state" class="hidden card text-center py-16">
<div class="text-6xl mb-4">📭</div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("check-circle", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3>
<p>No items in the sync queue</p> <p style="color: var(--text-secondary)">No items in the sync queue</p>
</div> </div>
<div id="queue-list" class="space-y-3"> <div id="queue-list" class="space-y-3">
if len(queueItems) == 0 { if len(queueItems) == 0 {
<div class="text-center py-16" style="color: var(--text-secondary)"> <div class="card text-center py-16">
<div class="text-6xl mb-4">📭</div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("check-circle", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3>
<p>No items in the sync queue</p> <p style="color: var(--text-secondary)">No items in the sync queue</p>
</div> </div>
} }
for _, item := range queueItems { for _, item := range queueItems {
<div class="card p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-4 transition-colors" style="cursor: default;">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2 gap-2">
<div class="flex items-center space-x-3"> <div class="flex items-center gap-2 flex-wrap">
<span class="inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }">{ item.Status }</span> <span class="badge status-{ item.Status }">{ item.Status }</span>
<span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span> <span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span>
</div> </div>
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-semibold priority-{ item.Priority }">P{ item.Priority }</span> <span class="badge priority-{ item.Priority }">P{ item.Priority }</span>
</div> </div>
<div class="text-sm mb-2" style="color: var(--text-secondary)"> <div class="text-sm mb-2" style="color: var(--text-secondary)">
Device ID: { item.DeviceID } Device ID: { item.DeviceID }
@@ -138,25 +142,29 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
<span>{ item.CreatedAt }</span> <span>{ item.CreatedAt }</span>
</div> </div>
if item.ErrorMessage != nil { if item.ErrorMessage != nil {
<div class="mt-2 text-sm" style="color: #ef4444;"> <div class="mt-2 text-sm flex items-start gap-2" style="color: var(--status-danger);">
Error: { *item.ErrorMessage } @Icon("alert", "h-4 w-4 shrink-0 mt-0.5")
<span>Error: { *item.ErrorMessage }</span>
</div> </div>
} }
</div> </div>
} }
</div> </div>
<div id="queue-item-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);"> <div id="queue-item-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-2xl my-8" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Queue Item Details</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Queue Item Details</h2>
<button @click="hideQueueItemModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);"> <button @click="hideQueueItemModal()" class="icon-btn" aria-label="Close">
X @Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
<div id="queue-item-details"></div> <div id="queue-item-details"></div>
<div class="flex justify-end space-x-3 mt-6"> <div class="flex justify-end space-x-3 mt-6">
<button @click="hideQueueItemModal()" class="btn-secondary px-4 py-2 rounded-lg">Close</button> <button @click="hideQueueItemModal()" class="btn btn-secondary">Close</button>
<button @click="retryQueueItem()" class="btn-primary px-4 py-2 rounded-lg">Retry</button> <button @click="retryQueueItem()" class="btn btn-primary">
@Icon("refresh", "h-4 w-4")
Retry
</button>
</div> </div>
</div> </div>
</div> </div>
+97 -33
View File
@@ -39,208 +39,272 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Sync Queue</h1><p style=\"color: var(--text-secondary)\">Monitor and manage offline sync queue</p></div><div class=\"flex space-x-3\"><button @click=\"processPendingItems()\" class=\"btn-primary px-4 py-2 rounded-lg\">Process Queue</button> <button @click=\"clearFailedItems()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Clear Failed</button> <button @click=\"clearAllItems()\" class=\"btn-danger px-4 py-2 rounded-lg\">Clear All</button></div></div><div id=\"stats-bar\" class=\"mt-4 grid grid-cols-2 md:grid-cols-4 gap-4\"><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #f59e0b;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex justify-between items-center gap-4 flex-wrap\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("clock", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Sync Queue</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Monitor and manage offline sync queue</p></div><div class=\"flex space-x-3\"><button @click=\"processPendingItems()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("play", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Process Queue</button> <button @click=\"clearFailedItems()\" class=\"btn btn-secondary\">Clear Failed</button> <button @click=\"clearAllItems()\" class=\"btn btn-danger\">Clear All</button></div></div><div id=\"stats-bar\" class=\"mt-4 grid grid-cols-2 md:grid-cols-4 gap-4\"><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-warning);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.PendingCount) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.PendingCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 38, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 40, Col: 97}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Pending</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #3b82f6;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Pending</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-info);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ProcessingCount) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ProcessingCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 42, Col: 86} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 44, Col: 97}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Processing</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #10b981;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Processing</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-success);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.CompletedCount) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.CompletedCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 46, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 48, Col: 99}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Completed</div></div><div class=\"card p-4 rounded-lg border text-center\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"text-3xl font-bold\" style=\"color: #ef4444;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Completed</div></div><div class=\"stat-card text-center\"><div class=\"text-3xl font-bold\" style=\"color: var(--status-danger);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.FailedCount) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.FailedCount)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 50, Col: 82} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 52, Col: 95}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"text-sm\" style=\"color: var(--text-secondary);\">Failed</div></div></div></div><div class=\"card p-4 rounded-lg border mb-6\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex flex-wrap gap-4 items-center\"><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Status Filter</label> <select id=\"status-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Items</option> <option value=\"pending\" selected>Pending</option> <option value=\"processing\">Processing</option> <option value=\"completed\">Completed</option> <option value=\"failed\">Failed</option></select></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Sync Type</label> <select id=\"type-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Types</option> <option value=\"progress\">Progress</option> <option value=\"note\">Notes</option> <option value=\"highlight\">Highlights</option> <option value=\"bookmark\">Bookmarks</option></select></div><div><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Device</label> <select id=\"device-filter\" onchange=\"filterQueue()\" class=\"px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"all\">All Devices</option></select></div><div class=\"flex-1\"></div><button @click=\"refreshQueue()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Refresh</button></div></div><div id=\"loading\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading queue...</p></div><div id=\"empty-state\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📭</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p>No items in the sync queue</p></div><div id=\"queue-list\" class=\"space-y-3\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Failed</div></div></div></div><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-end\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Status Filter</label> <select id=\"status-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Items</option> <option value=\"pending\" selected>Pending</option> <option value=\"processing\">Processing</option> <option value=\"completed\">Completed</option> <option value=\"failed\">Failed</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Sync Type</label> <select id=\"type-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Types</option> <option value=\"progress\">Progress</option> <option value=\"note\">Notes</option> <option value=\"highlight\">Highlights</option> <option value=\"bookmark\">Bookmarks</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Device</label> <select id=\"device-filter\" onchange=\"filterQueue()\" class=\"input\"><option value=\"all\">All Devices</option></select></div><div class=\"flex-1\"></div><button @click=\"refreshQueue()\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Refresh</button></div></div><div id=\"loading\" class=\"hidden text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading queue...</p></div><div id=\"empty-state\" class=\"hidden card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p style=\"color: var(--text-secondary)\">No items in the sync queue</p></div><div id=\"queue-list\" class=\"space-y-3\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(queueItems) == 0 { if len(queueItems) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📭</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p>No items in the sync queue</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">Queue Empty</h3><p style=\"color: var(--text-secondary)\">No items in the sync queue</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, item := range queueItems { for _, item := range queueItems {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"card p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-start mb-2\"><div class=\"flex items-center space-x-3\"><span class=\"inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"card p-4 transition-colors\" style=\"cursor: default;\"><div class=\"flex justify-between items-start mb-2 gap-2\"><div class=\"flex items-center gap-2 flex-wrap\"><span class=\"badge status-{ item.Status }\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 125, Col: 128} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 129, Col: 65}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span> <span class=\"text-sm\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> <span class=\"text-sm\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.SyncType) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.SyncType)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 126, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 130, Col: 83}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></div><span class=\"inline-block px-2 py-0.5 rounded text-[10px] font-semibold priority-{ item.Priority }\">P") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><span class=\"badge priority-{ item.Priority }\">P")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Priority) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Priority)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 128, Col: 124} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 132, Col: 71}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></div><div class=\"text-sm mb-2\" style=\"color: var(--text-secondary)\">Device ID: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div><div class=\"text-sm mb-2\" style=\"color: var(--text-secondary)\">Device ID: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceID) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 131, Col: 34} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 135, Col: 34}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.MediaTitle != nil { if item.MediaTitle != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "| Book: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "| Book: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(*item.MediaTitle) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(*item.MediaTitle)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 133, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div class=\"flex justify-between items-center text-sm\" style=\"color: var(--text-secondary)\"><span>Attempts: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"flex justify-between items-center text-sm\" style=\"color: var(--text-secondary)\"><span>Attempts: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Attempts) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Attempts)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 39} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 141, Col: 39}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "/") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "/")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.MaxAttempts) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.MaxAttempts)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 137, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 141, Col: 60}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> <span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> <span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 138, Col: 30} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 30}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if item.ErrorMessage != nil { if item.ErrorMessage != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"mt-2 text-sm\" style=\"color: #ef4444;\">Error: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mt-2 text-sm flex items-start gap-2\" style=\"color: var(--status-danger);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("alert", "h-4 w-4 shrink-0 mt-0.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span>Error: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(*item.ErrorMessage) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(*item.ErrorMessage)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 147, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div id=\"queue-item-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-2xl mx-4 my-8\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">Queue Item Details</h2><button @click=\"hideQueueItemModal()\" class=\"p-2 hover:opacity-80 rounded-lg\" style=\"color: var(--text-primary); background-color: var(--bg-primary);\">X</button></div><div id=\"queue-item-details\"></div><div class=\"flex justify-end space-x-3 mt-6\"><button @click=\"hideQueueItemModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Close</button> <button @click=\"retryQueueItem()\" class=\"btn-primary px-4 py-2 rounded-lg\">Retry</button></div></div></div></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div id=\"queue-item-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl my-8\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Queue Item Details</h2><button @click=\"hideQueueItemModal()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</button></div><div id=\"queue-item-details\"></div><div class=\"flex justify-end space-x-3 mt-6\"><button @click=\"hideQueueItemModal()\" class=\"btn btn-secondary\">Close</button> <button @click=\"retryQueueItem()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Retry</button></div></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -29,7 +29,7 @@ func Register() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register</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=\"register\" x-init=\"initRegisterTheme()\"><div class=\"container mx-auto px-4 py-8 max-w-md\"><h1 class=\"text-3xl font-bold text-center mb-8\">Register for Bookhoard</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 rounded-lg shadow-md border\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\"></div><div id=\"password-requirements\" class=\"mb-4 p-4 rounded-lg border text-sm\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><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 class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-1\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"w-full px-3 py-2 border rounded\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center\"></div><p class=\"text-center mt-4\"><a href=\"/login\" class=\"text-blue-500 hover:underline\">Already have an account? Login</a></p></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register - 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=\"register\" x-init=\"initRegisterTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4\"><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a><h1 class=\"text-xl font-bold text-center mb-6\" style=\"color: var(--text-primary)\">Create your account</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"input\" required></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"input\"></div></div><div id=\"password-requirements\" class=\"p-4 rounded-xl text-sm\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><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><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"input\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn btn-primary w-full py-2.5 opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Already have an account? <a href=\"/login\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Login</a></p></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -1,11 +1,18 @@
package templates package templates
templ RestoreSystemCollectionModal() { templ RestoreSystemCollectionModal() {
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70"> <div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Restore System Collection</h2> <div class="flex items-center gap-3">
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("refresh", "h-5 w-5")
</span>
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Restore System Collection</h2>
</div>
<button type="button" @click="closeCollectionModal()" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<p class="mb-4" style="color: var(--text-secondary)">Select a system collection to restore:</p> <p class="mb-4" style="color: var(--text-secondary)">Select a system collection to restore:</p>
@@ -13,7 +20,7 @@ templ RestoreSystemCollectionModal() {
hx-post="/api/dashboard/restore-system-collection" hx-post="/api/dashboard/restore-system-collection"
hx-redirect="/collections" hx-redirect="/collections"
> >
<select name="collection_name" class="w-full px-4 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"> <select name="collection_name" class="input">
<option value="">Select collection...</option> <option value="">Select collection...</option>
<option value="Continue Reading">Continue Reading</option> <option value="Continue Reading">Continue Reading</option>
<option value="Recently Added">Recently Added</option> <option value="Recently Added">Recently Added</option>
@@ -22,8 +29,8 @@ templ RestoreSystemCollectionModal() {
<option value="Continue Series">Continue Series</option> <option value="Continue Series">Continue Series</option>
</select> </select>
<div class="flex justify-end space-x-3 mt-6"> <div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeCollectionModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button> <button type="button" @click="closeCollectionModal()" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">Restore</button> <button type="submit" class="btn btn-primary">Restore</button>
</div> </div>
</form> </form>
</div> </div>
@@ -29,7 +29,23 @@ func RestoreSystemCollectionModal() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/70\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Restore System Collection</h2><button type=\"button\" @click=\"closeCollectionModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><div class=\"mb-4\"><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Select a system collection to restore:</p><form hx-post=\"/api/dashboard/restore-system-collection\" hx-redirect=\"/collections\"><select name=\"collection_name\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Select collection...</option> <option value=\"Continue Reading\">Continue Reading</option> <option value=\"Recently Added\">Recently Added</option> <option value=\"Recently Read\">Recently Read</option> <option value=\"Not Started\">Not Started</option> <option value=\"Continue Series\">Continue Series</option></select><div class=\"flex justify-end space-x-3 mt-6\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Restore</button></div></form></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"collections\" class=\"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\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Restore System Collection</h2></div><button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</button></div><div class=\"mb-4\"><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Select a system collection to restore:</p><form hx-post=\"/api/dashboard/restore-system-collection\" hx-redirect=\"/collections\"><select name=\"collection_name\" class=\"input\"><option value=\"\">Select collection...</option> <option value=\"Continue Reading\">Continue Reading</option> <option value=\"Recently Added\">Recently Added</option> <option value=\"Recently Read\">Recently Read</option> <option value=\"Not Started\">Not Started</option> <option value=\"Continue Series\">Continue Series</option></select><div class=\"flex justify-end space-x-3 mt-6\"><button type=\"button\" @click=\"closeCollectionModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Restore</button></div></form></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+26 -18
View File
@@ -17,15 +17,24 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
<body x-data="seriesPage" x-init="initSeriesPage()" class="theme-{ user.Theme }"> <body x-data="seriesPage" x-init="initSeriesPage()" class="theme-{ user.Theme }">
@Header(user, "/series") @Header(user, "/series")
@LibrarySwitcher(libData, currentLibraryID) @LibrarySwitcher(libData, currentLibraryID)
<!-- Series Grid -->
<div id="series-container"> <div id="series-container">
<main class="w-full px-4 py-8"> <main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold mb-6" style="color: var(--text-primary)">Series</h1> <div class="mb-6 flex items-center gap-3">
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("layers", "h-5 w-5")
</span>
<div>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Series</h1>
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Grouped by series metadata</p>
</div>
</div>
if len(seriesList) == 0 { if len(seriesList) == 0 {
<div id="series-empty" class="text-center py-16" style="color: var(--text-secondary)"> <div id="series-empty" class="card text-center py-16 px-6">
<div class="text-6xl mb-4">📚</div> <div class="grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("layers", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Series Found</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Series Found</h3>
<p>Books with series metadata will appear here</p> <p style="color: var(--text-secondary)">Books with series metadata will appear here</p>
</div> </div>
} }
<div id="series-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6"> <div id="series-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
@@ -33,28 +42,27 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
@SeriesCard(series) @SeriesCard(series)
} }
</div> </div>
<!-- Pagination -->
if totalPages > 1 { if totalPages > 1 {
<div id="series-pagination" class="flex justify-center items-center gap-4 mt-8"> <div id="series-pagination" class="flex justify-center items-center gap-3 mt-10">
if currentPage > 1 { if currentPage > 1 {
<a <a
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1) } href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1) }
class="px-4 py-2 rounded-lg border" class="btn btn-secondary"
style="border-color: var(--border); color: var(--text-primary);"
> >
Previous @Icon("chevron-left", "h-4 w-4")
<span>Previous</span>
</a> </a>
} }
<span class="text-sm" style="color: var(--text-secondary)"> <span class="chip">
Page { fmt.Sprintf("%d", currentPage) } of { fmt.Sprintf("%d", totalPages) } Page { fmt.Sprintf("%d", currentPage) } of { fmt.Sprintf("%d", totalPages) }
</span> </span>
if currentPage < totalPages { if currentPage < totalPages {
<a <a
href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1) } href={ fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1) }
class="px-4 py-2 rounded-lg border" class="btn btn-secondary"
style="border-color: var(--border); color: var(--text-primary);"
> >
Next <span>Next</span>
@Icon("chevron-right", "h-4 w-4")
</a> </a>
} }
</div> </div>
@@ -68,7 +76,7 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
templ SeriesCard(series SeriesCardData) { templ SeriesCard(series SeriesCardData) {
<a href={ "/series/detail?name=" + url.QueryEscape(series.Name) } class="block"> <a href={ "/series/detail?name=" + url.QueryEscape(series.Name) } class="block">
<div class="series-card rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow" style="background-color: var(--bg-secondary);"> <div class="series-card card rounded-2xl overflow-hidden cursor-pointer" style="box-shadow: var(--shadow-card);">
<div class={ "stacked-covers cover-count-" + fmt.Sprintf("%d", len(series.CoverPaths)) }> <div class={ "stacked-covers cover-count-" + fmt.Sprintf("%d", len(series.CoverPaths)) }>
for i, cover := range series.CoverPaths { for i, cover := range series.CoverPaths {
<img <img
@@ -80,8 +88,8 @@ templ SeriesCard(series SeriesCardData) {
/> />
} }
if len(series.CoverPaths) == 0 { if len(series.CoverPaths) == 0 {
<div class="cover-img cover-0 flex items-center justify-center" style="background: var(--bg-primary);"> <div class="cover-img cover-0 flex items-center justify-center" style="background: var(--bg-primary); color: var(--text-secondary);">
<span class="text-3xl">📚</span> @Icon("book", "h-8 w-8 opacity-60")
</div> </div>
} }
</div> </div>
+78 -38
View File
@@ -46,17 +46,33 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Series Grid --><div id=\"series-container\"><main class=\"w-full px-4 py-8\"><h1 class=\"text-3xl font-bold mb-6\" style=\"color: var(--text-primary)\">Series</h1>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div id=\"series-container\"><main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6 flex items-center gap-3\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("layers", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><div><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Series</h1><p class=\"text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Grouped by series metadata</p></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(seriesList) == 0 { if len(seriesList) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div id=\"series-empty\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Found</h3><p>Books with series metadata will appear here</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"series-empty\" class=\"card text-center py-16 px-6\"><div class=\"grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("layers", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Series Found</h3><p style=\"color: var(--text-secondary)\">Books with series metadata will appear here</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"series-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div id=\"series-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -66,89 +82,105 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><!-- Pagination -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if totalPages > 1 { if totalPages > 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div id=\"series-pagination\" class=\"flex justify-center items-center gap-4 mt-8\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div id=\"series-pagination\" class=\"flex justify-center items-center gap-3 mt-10\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if currentPage > 1 { if currentPage > 1 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 templ.SafeURL var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1)) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 41, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 93}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">← Previous</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Previous</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"text-sm\" style=\"color: var(--text-secondary)\">Page ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"chip\">Page ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage)) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 45} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 57, Col: 45}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " of ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " of ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages)) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 49, Col: 82} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 57, Col: 82}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if currentPage < totalPages { if currentPage < totalPages {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 templ.SafeURL var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1)) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 53, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 61, Col: 93}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"px-4 py-2 rounded-lg border\" style=\"border-color: var(--border); color: var(--text-primary);\">Next →</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"btn btn-secondary\"><span>Next</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</main></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -156,7 +188,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -185,20 +217,20 @@ func SeriesCard(series SeriesCardData) templ.Component {
templ_7745c5c3_Var6 = templ.NopComponent templ_7745c5c3_Var6 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 templ.SafeURL var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name)) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 70, Col: 64} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 78, Col: 64}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" class=\"block\"><div class=\"series-card rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"block\"><div class=\"series-card card rounded-2xl overflow-hidden cursor-pointer\" style=\"box-shadow: var(--shadow-card);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -207,7 +239,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -220,7 +252,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -230,20 +262,20 @@ func SeriesCard(series SeriesCardData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<img src=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<img src=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 75, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 83, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -256,70 +288,78 @@ func SeriesCard(series SeriesCardData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" alt=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" alt=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 77, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 85, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if len(series.CoverPaths) == 0 { if len(series.CoverPaths) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"cover-img cover-0 flex items-center justify-center\" style=\"background: var(--bg-primary);\"><span class=\"text-3xl\">📚</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"cover-img cover-0 flex items-center justify-center\" style=\"background: var(--bg-primary); color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book", "h-8 w-8 opacity-60").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div><div class=\"series-card-info px-3 py-2\"><h3 class=\"font-semibold text-sm line-clamp-2\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div><div class=\"series-card-info px-3 py-2\"><h3 class=\"font-semibold text-sm line-clamp-2\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 89, Col: 99} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 97, Col: 99}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</h3><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</h3><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount)) templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 91, Col: 42} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 99, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " of ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " of ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries)) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 91, Col: 89} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 99, Col: 89}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " books</p></div></div></a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " books</p></div></div></a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+113 -95
View File
@@ -12,21 +12,24 @@ templ Setup() {
<link href="/static/style.css" rel="stylesheet"/> <link href="/static/style.css" rel="stylesheet"/>
</head> </head>
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()"> <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="mx-auto container px-4 py-8 max-w-2xl">
<div class="text-center mb-8"> <div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2" style="color: var(--text-primary)">Welcome to Bookhoard</h1> <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> <p class="text-lg" style="color: var(--text-secondary)">Let's get your library set up.</p>
</div> </div>
<!-- Step indicator -->
<div class="flex items-center justify-center mb-8 gap-2"> <div class="flex items-center justify-center mb-8 gap-2">
<template x-for="n in 4" :key="n"> <template x-for="n in 4" :key="n">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all" 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)]'" :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> <template x-if="currentStep > n">@Icon("check", "h-5 w-5")</template>
<span x-show="currentStep <= n" x-text="n"></span>
</div> </div>
<div <div
x-text="['Admin', 'Libraries', 'Folders', 'Scan'][n-1]" x-text="['Admin', 'Libraries', 'Folders', 'Scan'][n-1]"
@@ -38,76 +41,73 @@ templ Setup() {
</template> </template>
</div> </div>
<!-- Error display --> <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);">
<div x-show="errorMsg" 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> <span x-text="errorMsg"></span>
</div> </div>
<!-- ==================== STEP 1: Create Admin ==================== --> <!-- ==================== STEP 1: Create Admin ==================== -->
<div x-show="currentStep === 1" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <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> <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> <p class="mb-6" style="color: var(--text-secondary)">This first account will have full admin privileges.</p>
<form @submit.prevent="submitAdmin()"> <form @submit.prevent="submitAdmin()">
<div class="mb-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="mb-6 card p-4" style="background-color: var(--bg-primary);">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label> <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 <input
type="url" type="url"
id="base-url"
x-model="admin.baseUrl" x-model="admin.baseUrl"
placeholder="https://books.example.com" placeholder="https://books.example.com"
class="w-full px-3 py-2 border rounded mt-2" class="input mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
required 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> <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>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label> <label for="email" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Email</label>
<input <input
type="email" type="email"
id="email" id="email"
x-model="admin.email" x-model="admin.email"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
required required
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label> <label for="username" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Username</label>
<input <input
type="text" type="text"
x-model="admin.username" x-model="admin.username"
id="username" id="username"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
required required
/> />
</div> </div>
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label> <label for="first-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
<input <input
type="text" type="text"
id="first-name"
x-model="admin.firstName" x-model="admin.firstName"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label> <label for="last-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
<input <input
type="text" type="text"
id="last-name"
x-model="admin.lastName" x-model="admin.lastName"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
/> />
</div> </div>
</div> </div>
<div id="password-requirements" class="mb-4 p-4 rounded-lg border text-sm" style="background-color: var(--bg-primary); border-color: var(--border)"> <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> <p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
<ul class="space-y-1" style="color: var(--text-secondary)"> <ul class="space-y-1" style="color: var(--text-secondary)">
<li id="req-length" class="flex items-center gap-2"> <li id="req-length" class="flex items-center gap-2">
@@ -131,48 +131,49 @@ templ Setup() {
</ul> </ul>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label> <label for="password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input <input
type="password" type="password"
x-model="admin.password" x-model="admin.password"
id="password" id="password"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
required required
/> />
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label> <label for="confirm-password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
<input <input
type="password" type="password"
x-model="admin.confirmPassword" x-model="admin.confirmPassword"
id="confirm-password" id="confirm-password"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
required required
/> />
</div> </div>
<button <button
type="submit" type="submit"
id="register-btn" id="register-btn"
class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed" class="btn btn-primary w-full"
:disabled="loading" :disabled="loading"
x-text="loading ? 'Creating...' : 'Create Admin Account'" x-text="loading ? 'Creating' : 'Create Admin Account'"
></button> ></button>
</form> </form>
</div> </div>
<!-- ==================== STEP 2: Create Libraries ==================== --> <!-- ==================== STEP 2: Create Libraries ==================== -->
<div x-show="currentStep === 2" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <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> <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> <p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
<!-- Created libraries list -->
<div x-show="libraries.length > 0" class="mb-6 space-y-2"> <div x-show="libraries.length > 0" class="mb-6 space-y-2">
<template x-for="(lib, idx) in libraries" :key="lib.id"> <template x-for="(lib, idx) in libraries" :key="lib.id">
<div class="p-3 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-3 flex items-center justify-between" style="background-color: var(--bg-primary);">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span> <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> <div>
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p> <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> <p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
@@ -181,65 +182,70 @@ templ Setup() {
<button <button
type="button" type="button"
@click="removeLibrary(idx)" @click="removeLibrary(idx)"
class="text-xs text-red-500 hover:underline" class="icon-btn"
>Remove</button> style="color: var(--status-danger);"
aria-label="Remove library"
>@Icon("trash", "h-4 w-4")</button>
</div> </div>
</template> </template>
</div> </div>
<!-- Add library form --> <form @submit.prevent="addLibrary()" class="space-y-4 card p-4" style="background-color: var(--bg-primary);">
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Name</label> <label for="lib-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
<input <input
type="text" type="text"
id="lib-name"
x-model="newLibrary.name" x-model="newLibrary.name"
placeholder="My Ebook Library" placeholder="My Ebook Library"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
required required
/> />
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Type</label> <label for="lib-type" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
<select <select
id="lib-type"
x-model="newLibrary.type" x-model="newLibrary.type"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
> >
<option value="ebooks">📚 Ebooks</option> <option value="ebooks">Ebooks</option>
<option value="comics">📖 Comics</option> <option value="comics">Comics</option>
<option value="manga">🗾 Manga</option> <option value="manga">Manga</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label> <label for="lib-desc" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
<input <input
type="text" type="text"
id="lib-desc"
x-model="newLibrary.description" x-model="newLibrary.description"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
/> />
</div> </div>
<button <button
type="submit" type="submit"
class="btn-secondary w-full py-2 rounded" class="btn btn-secondary w-full"
:disabled="loading || !newLibrary.name.trim()" :disabled="loading || !newLibrary.name.trim()"
x-text="loading ? 'Creating...' : '+ Add This Library'" x-text="loading ? 'Creating' : '+ Add This Library'"
></button> ></button>
</form> </form>
<div class="flex justify-between mt-6"> <div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 1" class="btn-secondary px-4 py-2 rounded">Back</button> <button type="button" @click="currentStep = 1" class="btn btn-secondary">
<button type="button" @click="currentStep = 3" class="btn-primary px-6 py-2 rounded"> @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> <span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip Continue'"></span>
@Icon("arrow-right", "h-4 w-4")
</button> </button>
</div> </div>
</div> </div>
<!-- ==================== STEP 3: Configure Folders ==================== --> <!-- ==================== STEP 3: Configure Folders ==================== -->
<div x-show="currentStep === 3" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <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> <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> <p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
@@ -249,40 +255,44 @@ templ Setup() {
<div class="space-y-6"> <div class="space-y-6">
<template x-for="lib in libraries" :key="lib.id"> <template x-for="lib in libraries" :key="lib.id">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-4" style="background-color: var(--bg-primary);">
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span> <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> <h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
</div> </div>
<!-- Existing folders for this library -->
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0"> <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"> <template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
<div class="flex items-center justify-between p-2 rounded text-sm" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="flex items-center justify-between p-2 rounded-lg text-sm" style="background-color: var(--bg-secondary);">
<span style="color: var(--text-primary)" x-text="folder"></span> <span class="flex items-center gap-2 truncate" style="color: var(--text-primary)">
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-red-500 hover:underline">Remove</button> @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> </div>
</template> </template>
</div> </div>
<!-- Add folder -->
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="text" type="text"
:placeholder="'/path/to/' + lib.type" :placeholder="'/path/to/' + lib.type"
:id="'folderInput-' + lib.id" :id="'folderInput-' + lib.id"
class="flex-1 px-3 py-2 border rounded text-sm" class="input flex-1 text-sm"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
/> />
<button <button
type="button" type="button"
@click="showFolderBrowser('folderInput-' + lib.id)" @click="showFolderBrowser('folderInput-' + lib.id)"
class="btn-secondary px-3 py-2 text-xs rounded" class="btn btn-secondary px-3 text-xs"
>Browse</button> >Browse</button>
<button <button
type="button" type="button"
@click="addFolderToLibrary(lib)" @click="addFolderToLibrary(lib)"
class="btn-primary px-3 py-2 text-xs rounded" class="btn btn-primary px-3 text-xs"
>Add</button> >Add</button>
</div> </div>
</div> </div>
@@ -290,35 +300,40 @@ templ Setup() {
</div> </div>
<div class="flex justify-between mt-6"> <div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 2" class="btn-secondary px-4 py-2 rounded">Back</button> <button type="button" @click="currentStep = 2" class="btn btn-secondary">
<button type="button" @click="currentStep = 4" class="btn-primary px-6 py-2 rounded">Next: Scan</button> @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>
</div> </div>
<!-- ==================== STEP 4: Scan & Finish ==================== --> <!-- ==================== STEP 4: Scan & Finish ==================== -->
<div x-show="currentStep === 4" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <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> <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> <p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
<!-- Summary -->
<div class="space-y-3 mb-6"> <div class="space-y-3 mb-6">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL</p> <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> <p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
</div> </div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Administrator</p> <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> <p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
</div> </div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-sm font-medium mb-2" style="color: var(--text-secondary)"> <p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">
Libraries (<span x-text="libraries.length"></span>) Libraries (<span x-text="libraries.length"></span>)
</p> </p>
<template x-for="lib in libraries" :key="lib.id"> <template x-for="lib in libraries" :key="lib.id">
<div class="flex items-start justify-between text-sm py-1"> <div class="flex items-start justify-between text-sm py-1">
<div> <div class="flex items-center gap-2">
<span style="color: var(--text-primary)" x-text="lib.name"></span> <span style="color: var(--text-primary)" x-text="lib.name"></span>
<span class="ml-2 text-xs px-2 py-0.5 rounded" style="background-color: var(--accent); color: var(--bg-primary)" x-text="lib.type"></span> <span class="chip" x-text="lib.type"></span>
</div> </div>
<div class="text-right" style="color: var(--text-secondary)"> <div class="text-right" style="color: var(--text-secondary)">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span> <span x-text="(lib.folders || []).length + ' folder(s)'"></span>
@@ -328,12 +343,11 @@ templ Setup() {
</div> </div>
</div> </div>
<!-- Scan progress -->
<div x-show="scanStarted" class="mb-6 space-y-3"> <div x-show="scanStarted" class="mb-6 space-y-3">
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3> <h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
<div id="library-progress-list" class="space-y-3"> <div id="library-progress-list" class="space-y-3">
<template x-for="job in scanJobs" :key="job.jobId"> <template x-for="job in scanJobs" :key="job.jobId">
<div class="p-3 rounded border" style="background-color: var(--bg-primary); border-color: var(--border)"> <div class="card p-3" style="background-color: var(--bg-primary);">
<div class="flex justify-between items-center mb-2"> <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="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> <span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
@@ -347,28 +361,34 @@ templ Setup() {
</div> </div>
</template> </template>
</div> </div>
<div x-show="scanComplete" class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm"> <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. Initial scan complete! Your libraries are ready to use.
</div> </div>
</div> </div>
<div class="flex justify-between mt-6"> <div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 3" class="btn-secondary px-4 py-2 rounded" :disabled="loading">Back</button> <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"> <div class="flex gap-3">
<button <button
x-show="!scanStarted" x-show="!scanStarted"
type="button" type="button"
@click="startScan()" @click="startScan()"
class="btn-primary px-6 py-2 rounded" class="btn btn-primary"
:disabled="loading" :disabled="loading"
>Start Scan</button> >
@Icon("sync", "h-4 w-4")
Start Scan
</button>
<button <button
x-show="scanStarted && !scanComplete" x-show="scanStarted && !scanComplete"
type="button" type="button"
@click="finishSetup()" @click="finishSetup()"
class="btn-secondary px-6 py-2 rounded" class="btn btn-secondary"
:disabled="loading" :disabled="loading"
>Skip to Dashboard</button> >Skip to Dashboard</button>
@@ -376,24 +396,22 @@ templ Setup() {
x-show="scanComplete" x-show="scanComplete"
type="button" type="button"
@click="finishSetup()" @click="finishSetup()"
class="btn-primary px-6 py-2 rounded" class="btn btn-primary"
:disabled="loading" :disabled="loading"
x-text="loading ? 'Finishing...' : 'Go to Dashboard'" x-text="loading ? 'Finishing' : 'Go to Dashboard'"
></button> ></button>
</div> </div>
</div> </div>
</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 p-4" style="background-color: var(--surface-overlay);">
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7)"> <div class="card p-6 w-full max-w-md mx-4">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" @click="hideFolderBrowser()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
</div> </div>
<div id="folder-browser-content"> <div id="folder-browser-content">
<!-- Directory listings rendered by Alpine -->
</div> </div>
</div> </div>
</div> </div>
File diff suppressed because one or more lines are too long
+73 -63
View File
@@ -12,107 +12,118 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
</head> </head>
<body class="theme-{ user.Theme }" x-data="unlinkedBooks" x-init="setupEventDelegation()"> <body class="theme-{ user.Theme }" x-data="unlinkedBooks" x-init="setupEventDelegation()">
@Header(user, "/unlinked") @Header(user, "/unlinked")
<div class="w-full px-4 sm:px-6 lg:px-8 py-8"> <main class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Unlinked Books</h1> <div class="flex items-center gap-3 mb-1">
<p style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p> <span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("sync", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Unlinked Books</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p>
</div> </div>
if len(unlinkedBooks) > 0 { if len(unlinkedBooks) > 0 {
<div class="flex justify-between items-center mb-6 p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-4 mb-6 flex flex-wrap justify-between items-center gap-3">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<input type="checkbox" id="select-all-unlinked" @change="toggleAllUnlinked()" class="w-5 h-5"/> <input type="checkbox" id="select-all-unlinked" @change="toggleAllUnlinked()" class="w-5 h-5"/>
<label for="select-all-unlinked" class="cursor-pointer" style="color: var(--text-primary)">Select All</label> <label for="select-all-unlinked" class="cursor-pointer text-sm font-medium" style="color: var(--text-primary)">Select All</label>
<span id="selected-count" class="text-sm" style="color: var(--text-secondary)">0 selected</span> <span id="selected-count" class="text-sm" style="color: var(--text-secondary)">0 selected</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2 flex-wrap">
<button @click="bulkAutoLink()" class="btn-secondary px-4 py-2 rounded-lg text-sm"> <button @click="bulkAutoLink()" class="btn btn-secondary text-sm">
🤖 Auto-Link Selected (High Confidence) @Icon("sync", "h-4 w-4")
Auto-Link Selected (High Confidence)
</button> </button>
<button @click="bulkGetSuggestions()" class="btn-secondary px-4 py-2 rounded-lg text-sm"> <button @click="bulkGetSuggestions()" class="btn btn-secondary text-sm">
💡 Get Suggestions for Selected @Icon("search", "h-4 w-4")
Get Suggestions for Selected
</button> </button>
<button @click="showBulkManualLink()" class="btn-primary px-4 py-2 rounded-lg text-sm"> <button @click="showBulkManualLink()" class="btn btn-primary text-sm">
🔗 Bulk Manual Link @Icon("edit", "h-4 w-4")
Bulk Manual Link
</button> </button>
</div> </div>
</div> </div>
} }
<div id="unlinked-container" class="space-y-6"> <div id="unlinked-container" class="space-y-6">
if len(unlinkedBooks) == 0 { if len(unlinkedBooks) == 0 {
<div class="text-center py-16" style="color: var(--text-secondary)"> <div class="card text-center py-16">
<div class="text-6xl mb-4"></div> <span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);">
@Icon("check-circle", "h-7 w-7")
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">All Books Linked!</h3> <h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">All Books Linked!</h3>
<p>There are no unlinked books that need manual resolution.</p> <p style="color: var(--text-secondary)">There are no unlinked books that need manual resolution.</p>
</div> </div>
} }
for _, book := range unlinkedBooks { for _, book := range unlinkedBooks {
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);"> <div class="card p-6">
<div class="flex gap-6"> <div class="flex gap-6">
<div class="flex items-start pt-2"> <div class="flex items-start pt-2">
<input type="checkbox" class="unlinked-checkbox w-5 h-5" data-progress-id="{ book.ProgressID }" data-title="{ book.TitleFromDevice }" onchange="updateSelectedCount()"/> <input type="checkbox" class="unlinked-checkbox w-5 h-5" data-progress-id="{ book.ProgressID }" data-title="{ book.TitleFromDevice }" onchange="updateSelectedCount()"/>
</div> </div>
<div class="flex-1"> <div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-4"> <div class="flex items-center gap-3 mb-4">
<div class="text-4xl"> <span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
if book.DeviceType == "koreader" { if book.DeviceType == "koreader" {
📖 @Icon("book-open", "h-5 w-5")
} else if book.DeviceType == "kobo" { } else if book.DeviceType == "kobo" {
📚 @Icon("library", "h-5 w-5")
} else { } else {
📱 @Icon("device", "h-5 w-5")
} }
</div> </span>
<div> <div class="min-w-0">
<h3 class="font-semibold text-lg" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3> <h3 class="font-semibold text-lg truncate" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3>
<p class="text-sm" style="color: var(--text-secondary)"> <p class="text-sm" style="color: var(--text-secondary)">
Device: { book.DeviceName } ({ book.DeviceType }) Device: { book.DeviceName } ({ book.DeviceType })
</p> </p>
</div> </div>
</div> </div>
<div class="space-y-2 text-sm mb-4"> <div class="space-y-2 text-sm mb-4">
<div> <div class="flex items-center gap-2 flex-wrap">
<span style="color: var(--text-secondary)">File Path:</span> <span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">File Path:</span>
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;"> <code class="chip font-mono">
@Icon("folder", "h-3.5 w-3.5")
{ book.FilePath } { book.FilePath }
</code> </code>
</div> </div>
if book.SHA256 != "" { if book.SHA256 != "" {
<div> <div class="flex items-center gap-2 flex-wrap">
<span style="color: var(--text-secondary)">SHA-256:</span> <span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">SHA-256:</span>
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;"> <code class="chip font-mono text-xs" title={ book.SHA256 }>{ book.SHA256 }</code>
{ book.SHA256 }
</code>
</div> </div>
} }
<div> <div class="flex items-center gap-2">
<span style="color: var(--text-secondary)">Last Synced:</span> <span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Last Synced:</span>
<span style="color: var(--text-primary)">{ book.LastSyncTime }</span> <span style="color: var(--text-primary)">{ book.LastSyncTime }</span>
</div> </div>
if book.ConfidenceScore > 0 { if book.ConfidenceScore > 0 {
<div> <div class="flex items-center gap-2">
<span style="color: var(--text-secondary)">Auto-Link Confidence:</span> <span class="text-xs uppercase tracking-wide font-medium" style="color: var(--text-secondary)">Auto-Link Confidence:</span>
<span style="color: var(--text-primary)"> <span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">
{ book.ConfidenceScore }% confidence { book.ConfidenceScore }% confidence
</span> </span>
</div> </div>
} }
</div> </div>
<div class="mb-4"> <div class="mb-4 flex gap-2 flex-wrap">
<button <button
@click="searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')" @click="searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')"
class="btn-secondary px-4 py-2 rounded-lg text-sm" class="btn btn-secondary text-sm"
> >
🔍 Find Potential Matches @Icon("search", "h-4 w-4")
Find Potential Matches
</button> </button>
<button <button
@click="showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')" @click="showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')"
class="btn-primary px-4 py-2 rounded-lg text-sm" class="btn btn-primary text-sm"
> >
🔗 Manually Link @Icon("edit", "h-4 w-4")
Manually Link
</button> </button>
</div> </div>
<div id="matches-{ book.ProgressID }" class="hidden"> <div id="matches-{ book.ProgressID }" class="hidden">
<div class="border-t pt-4" style="border-color: var(--border);"> <div class="border-t pt-4 mt-2" style="border-color: var(--border);">
<h4 class="font-semibold mb-3" style="color: var(--text-primary)">Potential Matches</h4> <h4 class="font-semibold mb-3" style="color: var(--text-primary)">Potential Matches</h4>
<div id="matches-list-{ book.ProgressID }" class="space-y-3"> <div id="matches-list-{ book.ProgressID }" class="space-y-3">
<p style="color: var(--text-secondary)">Click "Find Potential Matches" to search</p> <p style="color: var(--text-secondary)">Click "Find Potential Matches" to search</p>
@@ -124,50 +135,49 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
</div> </div>
} }
</div> </div>
</div> </main>
<!-- Manual Link Modal --> <div id="manual-link-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div id="manual-link-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);"> <div class="card p-6 w-full max-w-md mx-4">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Manually Link Book</h2> <h2 class="text-xl font-bold" style="color: var(--text-primary)">Manually Link Book</h2>
<button @click="hideManualLinkModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button @click="hideManualLinkModal()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
</div> </div>
<input type="hidden" id="link-progress-id"/> <input type="hidden" id="link-progress-id"/>
<input type="hidden" id="link-book-sha256"/> <input type="hidden" id="link-book-sha256"/>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Book Title from Device</label> <label for="link-book-title" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Book Title from Device</label>
<input <input
type="text" type="text"
id="link-book-title" id="link-book-title"
readonly readonly
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);" style="color: var(--text-secondary);"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search for Book to Link</label> <label for="link-search-input" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Search for Book to Link</label>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="text" type="text"
id="link-search-input" id="link-search-input"
placeholder="Search by title, author..." placeholder="Search by title, author"
class="flex-1 px-4 py-2 border rounded-lg" class="input flex-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
<button <button
type="button" type="button"
@click="searchBooksForLink()" @click="searchBooksForLink()"
class="btn-primary px-4 py-2 rounded-lg" class="btn btn-primary"
> >
@Icon("search", "h-4 w-4")
Search Search
</button> </button>
</div> </div>
</div> </div>
<div id="link-search-results" class="mb-4 max-h-48 overflow-y-auto space-y-2"> <div id="link-search-results" class="mb-4 max-h-48 overflow-y-auto space-y-2">
<p style="color: var(--text-secondary)">Search for books to link</p> <p class="text-sm" style="color: var(--text-secondary)">Search for books to link</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Confidence Score</label> <label for="link-confidence" class="block text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">Confidence Score</label>
<input <input
type="number" type="number"
id="link-confidence" id="link-confidence"
@@ -175,16 +185,16 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
max="1" max="1"
step="0.1" step="0.1"
value="1.0" value="1.0"
class="w-full px-4 py-2 border rounded-lg" class="input"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
/> />
<p class="text-xs mt-1" style="color: var(--text-secondary)">1.0 = manual override (highest confidence)</p> <p class="text-xs mt-1" style="color: var(--text-secondary)">1.0 = manual override (highest confidence)</p>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end gap-3">
<button type="button" @click="hideManualLinkModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideManualLinkModal()" class="btn btn-secondary">
Cancel Cancel
</button> </button>
<button type="button" @click="confirmManualLink()" class="btn-primary px-4 py-2 rounded-lg"> <button type="button" @click="confirmManualLink()" class="btn btn-primary">
@Icon("check", "h-4 w-4")
Link Book Link Book
</button> </button>
</div> </div>
+133 -36
View File
@@ -37,164 +37,261 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">Unlinked Books</h1><p style=\"color: var(--text-secondary)\">Resolve books that couldn't be automatically matched between devices</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"mx-auto container px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Unlinked Books</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Resolve books that couldn't be automatically matched between devices</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(unlinkedBooks) > 0 { if len(unlinkedBooks) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex justify-between items-center mb-6 p-4 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex items-center gap-4\"><input type=\"checkbox\" id=\"select-all-unlinked\" @change=\"toggleAllUnlinked()\" class=\"w-5 h-5\"> <label for=\"select-all-unlinked\" class=\"cursor-pointer\" style=\"color: var(--text-primary)\">Select All</label> <span id=\"selected-count\" class=\"text-sm\" style=\"color: var(--text-secondary)\">0 selected</span></div><div class=\"flex gap-2\"><button @click=\"bulkAutoLink()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🤖 Auto-Link Selected (High Confidence)</button> <button @click=\"bulkGetSuggestions()\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">💡 Get Suggestions for Selected</button> <button @click=\"showBulkManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Bulk Manual Link</button></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-4 mb-6 flex flex-wrap justify-between items-center gap-3\"><div class=\"flex items-center gap-4\"><input type=\"checkbox\" id=\"select-all-unlinked\" @change=\"toggleAllUnlinked()\" class=\"w-5 h-5\"> <label for=\"select-all-unlinked\" class=\"cursor-pointer text-sm font-medium\" style=\"color: var(--text-primary)\">Select All</label> <span id=\"selected-count\" class=\"text-sm\" style=\"color: var(--text-secondary)\">0 selected</span></div><div class=\"flex gap-2 flex-wrap\"><button @click=\"bulkAutoLink()\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Auto-Link Selected (High Confidence)</button> <button @click=\"bulkGetSuggestions()\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Get Suggestions for Selected</button> <button @click=\"showBulkManualLink()\" class=\"btn btn-primary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Bulk Manual Link</button></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"unlinked-container\" class=\"space-y-6\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div id=\"unlinked-container\" class=\"space-y-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if len(unlinkedBooks) == 0 { if len(unlinkedBooks) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">✓</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">All Books Linked!</h3><p>There are no unlinked books that need manual resolution.</p></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"card text-center py-16\"><span class=\"grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl\" style=\"background-color: color-mix(in srgb, var(--status-success) 16%, transparent); color: var(--status-success);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-7 w-7").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">All Books Linked!</h3><p style=\"color: var(--text-secondary)\">There are no unlinked books that need manual resolution.</p></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
for _, book := range unlinkedBooks { for _, book := range unlinkedBooks {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"card p-6 rounded-lg border\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex gap-6\"><div class=\"flex items-start pt-2\"><input type=\"checkbox\" class=\"unlinked-checkbox w-5 h-5\" data-progress-id=\"{ book.ProgressID }\" data-title=\"{ book.TitleFromDevice }\" onchange=\"updateSelectedCount()\"></div><div class=\"flex-1\"><div class=\"flex items-center gap-3 mb-4\"><div class=\"text-4xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card p-6\"><div class=\"flex gap-6\"><div class=\"flex items-start pt-2\"><input type=\"checkbox\" class=\"unlinked-checkbox w-5 h-5\" data-progress-id=\"{ book.ProgressID }\" data-title=\"{ book.TitleFromDevice }\" onchange=\"updateSelectedCount()\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex items-center gap-3 mb-4\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if book.DeviceType == "koreader" { if book.DeviceType == "koreader" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "📖") templ_7745c5c3_Err = Icon("book-open", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else if book.DeviceType == "kobo" { } else if book.DeviceType == "kobo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "📚") templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "📱") templ_7745c5c3_Err = Icon("device", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><div><h3 class=\"font-semibold text-lg\" style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span><div class=\"min-w-0\"><h3 class=\"font-semibold text-lg truncate\" style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 66, Col: 102} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 76, Col: 111}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Device: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Device: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 68, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 78, Col: 37}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " (") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " (")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 68, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 78, Col: 58}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div><span style=\"color: var(--text-secondary)\">File Path:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div class=\"flex items-center gap-2 flex-wrap\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">File Path:</span> <code class=\"chip font-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("folder", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 76, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 87, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</code></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if book.SHA256 != "" { if book.SHA256 != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div><span style=\"color: var(--text-secondary)\">SHA-256:</span> <code style=\"color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"flex items-center gap-2 flex-wrap\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">SHA-256:</span> <code class=\"chip font-mono text-xs\" title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.SHA256)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 83, Col: 26} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 93, Col: 68}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 93, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</code></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div><span style=\"color: var(--text-secondary)\">Last Synced:</span> <span style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"flex items-center gap-2\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Last Synced:</span> <span style=\"color: var(--text-primary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 89, Col: 71} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 98, Col: 71}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if book.ConfidenceScore > 0 { if book.ConfidenceScore > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div><span style=\"color: var(--text-secondary)\">Auto-Link Confidence:</span> <span style=\"color: var(--text-primary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"flex items-center gap-2\"><span class=\"text-xs uppercase tracking-wide font-medium\" style=\"color: var(--text-secondary)\">Auto-Link Confidence:</span> <span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 95, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 104, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "% confidence</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "% confidence</span></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"mb-4\"><button @click=\"searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')\" class=\"btn-secondary px-4 py-2 rounded-lg text-sm\">🔍 Find Potential Matches</button> <button @click=\"showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')\" class=\"btn-primary px-4 py-2 rounded-lg text-sm\">🔗 Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t pt-4\" style=\"border-color: var(--border);\"><h4 class=\"font-semibold mb-3\" style=\"color: var(--text-primary)\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p style=\"color: var(--text-secondary)\">Click \"Find Potential Matches\" to search</p></div></div></div></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"mb-4 flex gap-2 flex-wrap\"><button @click=\"searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')\" class=\"btn btn-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Find Potential Matches</button> <button @click=\"showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')\" class=\"btn btn-primary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("edit", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t pt-4 mt-2\" style=\"border-color: var(--border);\"><h4 class=\"font-semibold mb-3\" style=\"color: var(--text-primary)\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p style=\"color: var(--text-secondary)\">Click \"Find Potential Matches\" to search</p></div></div></div></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div><!-- Manual Link Modal --><div id=\"manual-link-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Manually Link Book</h2><button @click=\"hideManualLinkModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Search for Book to Link</label><div class=\"flex gap-2\"><input type=\"text\" id=\"link-search-input\" placeholder=\"Search by title, author...\" class=\"flex-1 px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"> <button type=\"button\" @click=\"searchBooksForLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p style=\"color: var(--text-secondary)\">Search for books to link</p></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideManualLinkModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" @click=\"confirmManualLink()\" class=\"btn-primary px-4 py-2 rounded-lg\">Link Book</button></div></div></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div></main><div id=\"manual-link-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-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Manually Link Book</h2><button @click=\"hideManualLinkModal()\" class=\"icon-btn\" aria-label=\"Close\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("close", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label for=\"link-book-title\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"input\" style=\"color: var(--text-secondary);\"></div><div class=\"mb-4\"><label for=\"link-search-input\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Search for Book to Link</label><div class=\"flex gap-2\"><input type=\"text\" id=\"link-search-input\" placeholder=\"Search by title, author…\" class=\"input flex-1\"> <button type=\"button\" @click=\"searchBooksForLink()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("search", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Search for books to link</p></div><div class=\"mb-4\"><label for=\"link-confidence\" class=\"block text-xs uppercase tracking-wide font-medium mb-2\" style=\"color: var(--text-secondary)\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"input\"><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end gap-3\"><button type=\"button\" @click=\"hideManualLinkModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" @click=\"confirmManualLink()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Link Book</button></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }