feat(ui): refresh auth, entry, admin, and secondary pages

Apply the new design-system primitives across the remaining pages so the
whole app shares one visual language:

- Auth/entry: redesigned index, login, register (centered card layout,
  SVG icons, semantic tokens)
- Admin: sidebar nav with icons, cohesive admin dashboard/users/library/
  processing-issues/settings (tables use border-line + hover states;
  scan/websocket attributes preserved)
- Secondary: analytics (stat cards), devices, progress, conflicts, queue,
  profile + form/modal all migrated to .card/.btn/.input primitives and
  SVG action icons
- Docs + API explorer + setup wizard + collection/rules/modals +
  unlinked books + book-detail modals: consistent chrome, modal scrims
  use --surface-overlay, close buttons use icon-btn
- reader.templ and error.templ intentionally left unchanged
This commit is contained in:
2026-08-05 16:01:22 -04:00
parent 28e40e302c
commit a075a5061f
54 changed files with 2740 additions and 2128 deletions
+37 -36
View File
@@ -11,76 +11,77 @@ 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 min-h-screen bg-surface">
@AdminSidebar(user, "/admin") @AdminSidebar(user, "/admin")
<main class="flex-1 p-8"> <main class="flex-1 p-6 sm: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> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">Dashboard</h1>
<p style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p> <p class="text-content-muted">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-8">
<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-center space-x-3"> <div class="flex items-center space-x-3">
<div class="text-3xl">📖</div> <div class="text-content-muted">@Icon("library", "h-7 w-7")</div>
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3> <h3 class="font-semibold text-content">Library</h3>
<p style="color: var(--text-secondary)" class="text-sm">Manage your ebook collection</p> <p class="text-content-muted text-sm">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">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="card p-6">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="text-3xl">👁️</div> <div class="text-content-muted">@Icon("sync", "h-7 w-7")</div>
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Watch Status</h3> <h3 class="font-semibold text-content">Scan Watch Status</h3>
<p style="color: var(--text-secondary)" class="text-sm">Auto-detecting new files</p> <p class="text-content-muted text-sm">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 text-content-muted">
<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 bg-green-500 mr-2"></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-xl font-semibold mb-4 text-content">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 p-4 text-left">
<div class="font-medium">Rescan Library</div> <div class="font-medium">Rescan Library</div>
<div style="color: var(--text-secondary)" class="text-sm">Re-scan existing files and fix metadata</div> <div class="text-content-muted text-sm">Re-scan existing files and fix metadata</div>
</button> </button>
<a href="/admin/library" class="btn-secondary p-4 rounded-lg text-left block"> <a href="/admin/library" class="btn btn-secondary p-4 text-left block">
<div class="font-medium">Manage Libraries and Folders</div> <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> <div class="text-content-muted text-sm">Add or remove libraries and scan directories</div>
</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 mt-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 text-content flex items-center gap-2">
📚 Scanning Libraries @Icon("library", "h-5 w-5")
Scanning Libraries
</h3> </h3>
<button @click="hideScanProgress()" class="p-2 hover:bg-gray-700 rounded"> <button @click="hideScanProgress()" class="p-2 hover:bg-surface-hover rounded">
@Icon("close", "h-4 w-4")
</button> </button>
</div> </div>
<!-- Overall Progress --> <!-- Overall Progress -->
<div class="mb-4"> <div class="mb-4">
<div class="flex justify-between text-sm mb-2"> <div class="flex justify-between text-sm mb-2">
<span style="color: var(--text-secondary)">Overall Progress</span> <span class="text-content-muted">Overall Progress</span>
<span id="scan-progress-text" style="color: var(--text-primary)">0%</span> <span id="scan-progress-text" class="text-content">0%</span>
</div> </div>
<div class="w-full bg-gray-700 rounded-full h-3"> <div class="w-full bg-surface-hover rounded-full h-3">
<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 bg-brand"
style="width: 0%; background-color: var(--accent);" style="width: 0%;"
></div> ></div>
</div> </div>
<div id="scan-status" class="text-sm mt-2" style="color: var(--text-secondary)"> <div id="scan-status" class="text-sm mt-2 text-content-muted">
Starting scan... Starting scan...
</div> </div>
</div> </div>
@@ -89,21 +90,21 @@ 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-lg border border-line bg-surface">
<h4 class="font-semibold mb-2" style="color: var(--text-primary)"> Scan Complete!</h4> <h4 class="font-semibold mb-2 text-content flex items-center gap-2">@Icon("check-circle", "h-4 w-4") Scan Complete!</h4>
<div id="scan-results-content" style="color: var(--text-secondary)"> <div id="scan-results-content" class="text-content-muted">
<!-- 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>
+48 -46
View File
@@ -10,49 +10,51 @@ 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 min-h-screen bg-surface">
@AdminSidebar(user, "/admin/library") @AdminSidebar(user, "/admin/library")
<main class="flex-1 p-8"> <main class="flex-1 p-6 sm:p-8">
<div class="w-full"> <div class="w-full">
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium"> <a href="/admin" class="btn btn-secondary font-medium">
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 font-medium">
+ 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> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">Library Management</h1>
<p style="color: var(--text-secondary)">Manage libraries and configure media scanning</p> <p class="text-content-muted">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-8">
<!-- 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> <h3 class="text-xl font-semibold mb-6 text-content">Libraries</h3>
<p style="color: var(--text-secondary)" class="mb-4">Manage media libraries and their folders</p> <p class="text-content-muted mb-4">Manage media libraries and their folders</p>
<div id="libraries-list" class="space-y-3 mb-6"> <div id="libraries-list" class="space-y-3 mb-6">
if len(libraries) == 0 { if len(libraries) == 0 {
<p style="color: var(--text-secondary)" class="text-center py-8"> <p class="text-content-muted text-center py-8">
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 border border-line rounded-lg bg-surface">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
<div> <div>
<h4 class="font-semibold" style="color: var(--text-primary)">{ library.Name }</h4> <h4 class="font-semibold text-content">{ 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 text-content-muted">{ 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="badge bg-brand text-white">
{ library.TypeName } { library.TypeName }
</span> </span>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<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">Folders</button>
<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> <button data-library-id={ library.ID } data-action="edit" class="btn btn-primary text-xs">Edit</button>
<button data-library-id={ library.ID } data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button> <button data-library-id={ library.ID } data-action="delete" class="btn btn-danger text-xs">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 +64,20 @@ 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> <h3 class="text-xl font-semibold mb-6 text-content">Library Visibility</h3>
<p style="color: var(--text-secondary)" class="mb-4">Control which libraries are visible to users</p> <p class="text-content-muted mb-4">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-8 card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">User Library Access</h3> <h3 class="text-xl font-semibold mb-6 text-content">User Library Access</h3>
<p style="color: var(--text-secondary)" class="mb-4">Manage individual user access to specific libraries</p> <p class="text-content-muted mb-4">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">
<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>
@@ -91,42 +93,42 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</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" 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="card p-6 w-full max-w-md mx-4">
<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 text-content">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="p-2 hover:bg-surface-hover rounded text-content">@Icon("close", "h-4 w-4")</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-sm font-medium mb-2 text-content">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-sm font-medium mb-2 text-content">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-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Type</label> <label class="block text-sm font-medium mb-2 text-content">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">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" 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="card p-6 w-full max-w-md mx-4">
<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 text-content">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="p-2 hover:bg-surface-hover rounded text-content">@Icon("close", "h-4 w-4")</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 -->
@@ -135,17 +137,17 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</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" 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="card p-6 w-full max-w-md mx-4">
<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 text-content">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="p-2 hover:bg-surface-hover rounded text-content">@Icon("close", "h-4 w-4")</button>
</div> </div>
<div id="delete-modal-content" class="mb-6" style="color: var(--text-primary)"> <div id="delete-modal-content" class="mb-6 text-content">
<!-- 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">Delete</button>
</div> </div>
</div> </div>
</div> </div>
+67 -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 min-h-screen bg-surface\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,161 +45,201 @@ 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-6 sm:p-8\"><div class=\"w-full\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><a href=\"/admin\" class=\"btn btn-secondary font-medium\">")
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 font-medium\">")
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><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">Library Management</h1><p class=\"text-content-muted\">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\"><h3 class=\"text-xl font-semibold mb-6 text-content\">Libraries</h3><p class=\"text-content-muted mb-4\">Manage media libraries and their folders</p><div id=\"libraries-list\" class=\"space-y-3 mb-6\">")
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, 6, "<p class=\"text-content-muted text-center py-8\">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, 7, "<div class=\"p-4 border border-line rounded-lg bg-surface\"><div class=\"flex justify-between items-start mb-2\"><div><h4 class=\"font-semibold text-content\">")
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: 46, Col: 66}
} }
_, 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, 8, "</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, 9, "<p class=\"text-sm text-content-muted\">")
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: 48, Col: 73}
} }
_, 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, 10, "</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, 11, "<span class=\"badge bg-brand text-white\">")
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: 51, Col: 32}
} }
_, 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, 12, "</span></div><div class=\"flex space-x-2\"><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: 55, Col: 49}
} }
_, 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, 13, "\" data-action=\"show-folders\" class=\"btn btn-secondary text-xs\">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: 56, Col: 49}
} }
_, 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, 14, "\" data-action=\"edit\" class=\"btn btn-primary text-xs\">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: 57, Col: 49}
} }
_, 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, 15, "\" data-action=\"delete\" class=\"btn btn-danger text-xs\">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: 60, Col: 52}
} }
_, 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, 16, "\" 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, 17, "</div></div><!-- User Library Visibility Section --><div class=\"card p-6\"><h3 class=\"text-xl font-semibold mb-6 text-content\">Library Visibility</h3><p class=\"text-content-muted 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\"><h3 class=\"text-xl font-semibold mb-6 text-content\">User Library Access</h3><p class=\"text-content-muted mb-4\">Manage individual user access to specific libraries</p><div class=\"mb-4\"><select id=\"user-select\" onchange=\"loadUserVisibility()\" class=\"input\"><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, 18, "<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: 83, Col: 52}
} }
_, 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, 19, " (")
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: 83, Col: 68}
} }
_, 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, 20, ")</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, 21, "</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 p-6 w-full max-w-md mx-4\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold text-content\">Create Library</h2><button type=\"button\" data-action=\"hide-create-modal\" class=\"p-2 hover:bg-surface-hover rounded text-content\">")
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, 22, "</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 text-content\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"input\" required></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2 text-content\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"input\"></textarea></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2 text-content\">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\">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 p-6 w-full max-w-md mx-4\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold text-content\">Browse Folders</h2><button type=\"button\" data-action=\"browse-cancel\" class=\"p-2 hover:bg-surface-hover rounded text-content\">")
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, 23, "</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 p-6 w-full max-w-md mx-4\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold text-content\">Delete Library</h2><button type=\"button\" data-action=\"hide-delete-modal\" class=\"p-2 hover:bg-surface-hover rounded text-content\">")
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, 24, "</button></div><div id=\"delete-modal-content\" class=\"mb-6 text-content\"><!-- 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\">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
} }
+20 -20
View File
@@ -11,15 +11,16 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
</head> </head>
<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-6 sm:p-8">
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div> <div>
<h1 class="text-3xl font-bold mb-2">Processing Issues</h1> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">Processing Issues</h1>
<p class="text-gray-600">Items that couldn't be processed in this library</p> <p class="text-content-muted">Items that couldn't be processed in this library</p>
</div> </div>
<a href="/admin/libraries/{ libraryID }" class="btn-secondary px-4 py-2 rounded-lg"> <a href="/admin/libraries/{ libraryID }" class="btn btn-secondary">
Back to Library @Icon("arrow-left", "h-4 w-4")
Back to Library
</a> </a>
</div> </div>
</div> </div>
@@ -27,39 +28,39 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
<!-- 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="card p-6 rounded-lg border-l-4 border-red-500"> <div class="card p-6 border-l-4 border-red-500">
<h3 class="text-lg font-semibold text-red-600 mb-2">Errors</h3> <h3 class="text-lg font-semibold text-red-600 mb-2">Errors</h3>
<p class="text-3xl font-bold">{ stats.ErrorCount }</p> <p class="text-3xl font-bold text-content">{ stats.ErrorCount }</p>
</div> </div>
} }
if stats.WarningCount > 0 { if stats.WarningCount > 0 {
<div class="card p-6 rounded-lg border-l-4 border-yellow-500"> <div class="card p-6 border-l-4 border-yellow-500">
<h3 class="text-lg font-semibold text-yellow-600 mb-2">Warnings</h3> <h3 class="text-lg font-semibold text-yellow-600 mb-2">Warnings</h3>
<p class="text-3xl font-bold">{ stats.WarningCount }</p> <p class="text-3xl font-bold text-content">{ stats.WarningCount }</p>
</div> </div>
} }
if stats.InfoCount > 0 { if stats.InfoCount > 0 {
<div class="card p-6 rounded-lg border-l-4 border-blue-500"> <div class="card p-6 border-l-4 border-blue-500">
<h3 class="text-lg font-semibold text-blue-600 mb-2">Info</h3> <h3 class="text-lg font-semibold text-blue-600 mb-2">Info</h3>
<p class="text-3xl font-bold">{ stats.InfoCount }</p> <p class="text-3xl font-bold text-content">{ stats.InfoCount }</p>
</div> </div>
} }
</div> </div>
} }
if len(issues) == 0 { if len(issues) == 0 {
<div class="card p-8 rounded-lg text-center"> <div class="card p-8 text-center">
<p class="text-gray-600">No processing issues found for this library.</p> <p class="text-content-muted">No processing issues found for this library.</p>
</div> </div>
} else { } else {
<!-- Issues List --> <!-- Issues List -->
<div class="space-y-4"> <div class="space-y-4">
for _, issue := range issues { for _, issue := range issues {
<div class="card p-6 rounded-lg"> <div class="card p-6">
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4">
<div class="flex-1"> <div class="flex-1">
<h4 class="text-lg font-semibold mb-2">{ issue.Title }</h4> <h4 class="text-lg font-semibold mb-2 text-content">{ issue.Title }</h4>
<p class="text-gray-700 mb-3">{ issue.IssueDescription }</p> <p class="text-content mb-3">{ issue.IssueDescription }</p>
<div class="text-sm text-gray-500 space-y-1"> <div class="text-sm text-content-muted space-y-1">
<p><strong>Type:</strong> { issue.IssueType }</p> <p><strong>Type:</strong> { issue.IssueType }</p>
<p><strong>Format:</strong> { issue.FormatGroup }</p> <p><strong>Format:</strong> { issue.FormatGroup }</p>
<p><strong>File:</strong> { issue.FilePath }</p> <p><strong>File:</strong> { issue.FilePath }</p>
@@ -68,8 +69,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
</div> </div>
<div class="ml-4"> <div class="ml-4">
<span <span
class="inline-block px-3 py-1 text-sm rounded-full font-medium" class="inline-block px-3 py-1 text-sm rounded-full font-medium bg-brand text-white"
style="background-color: var(--accent); color: white;"
> >
{ issue.Severity } { issue.Severity }
</span> </span>
@@ -79,7 +79,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
if issue.Severity == "warning" || issue.Severity == "info" { if issue.Severity == "warning" || issue.Severity == "info" {
<button <button
@click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')" @click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
class="btn-secondary px-4 py-2 rounded text-sm" class="btn btn-secondary text-sm"
> >
Dismiss Dismiss
</button> </button>
+40 -32
View File
@@ -38,167 +38,175 @@ 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-6 sm:p-8\"><div class=\"mb-8\"><div class=\"flex items-center justify-between mb-4\"><div><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">Processing Issues</h1><p class=\"text-content-muted\">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, 3, "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, 4, "<!-- 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, 5, "<div class=\"card p-6 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 text-content\">")
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: 33, Col: 69}
} }
_, 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, 6, "</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, 7, "<div class=\"card p-6 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 text-content\">")
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: 39, Col: 71}
} }
_, 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, 8, "</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, 9, "<div class=\"card p-6 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 text-content\">")
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: 45, Col: 68}
} }
_, 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, 10, "</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, 11, "</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, 12, "<div class=\"card p-8 text-center\"><p class=\"text-content-muted\">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, 13, "<!-- 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, 14, "<div class=\"card p-6\"><div class=\"flex justify-between items-start mb-4\"><div class=\"flex-1\"><h4 class=\"text-lg font-semibold mb-2 text-content\">")
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: 61, Col: 75}
} }
_, 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, 15, "</h4><p class=\"text-content mb-3\">")
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: 62, Col: 63}
} }
_, 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, 16, "</p><div class=\"text-sm text-content-muted space-y-1\"><p><strong>Type:</strong> ")
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: 64, Col: 54}
} }
_, 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, 17, "</p><p><strong>Format:</strong> ")
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: 65, Col: 58}
} }
_, 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, 18, "</p><p><strong>File:</strong> ")
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: 66, 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, 18, "</p><p><strong>Library:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p><p><strong>Library:</strong> ")
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: 67, Col: 63}
} }
_, 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, 20, "</p></div></div><div class=\"ml-4\"><span class=\"inline-block px-3 py-1 text-sm rounded-full font-medium bg-brand text-white\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -211,27 +219,27 @@ 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, 20, "</span></div></div><div class=\"flex gap-3 mt-4\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></div></div><div class=\"flex gap-3 mt-4\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if issue.Severity == "warning" || issue.Severity == "info" { 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, 22, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn btn-secondary text-sm\">Dismiss</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, 22, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</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, 24, "</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, 25, "</main></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+25 -26
View File
@@ -11,55 +11,54 @@ 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 min-h-screen bg-surface">
@AdminSidebar(user, "/admin/settings") @AdminSidebar(user, "/admin/settings")
<main class="flex-1 p-8"> <main class="flex-1 p-6 sm: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 mb-4">
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium"> <a href="/admin" class="btn btn-secondary font-medium">
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> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">System Settings</h1>
<p style="color: var(--text-secondary)">Configure your Bookhoard instance</p> <p class="text-content-muted">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-lg border border-line bg-surface-raised text-danger">
{ errorMessage } { errorMessage }
</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> <h3 class="text-xl font-semibold mb-6 text-content">Base URL</h3>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Base URL</label> <label class="block text-sm font-medium mb-2 text-content">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-1 text-content-muted">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 font-medium">
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-8 card p-6">
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">System Defaults</h3> <h3 class="text-xl font-semibold mb-6 text-content">System Defaults</h3>
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Default Timezone</label> <label class="block text-sm font-medium mb-2 text-content">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 +85,16 @@ 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-1 text-content-muted">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-8 card p-6">
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">URL Paths</h3> <h3 class="text-xl font-semibold mb-4 text-content">URL Paths</h3>
<div class="space-y-2 text-sm" style="color: var(--text-secondary);"> <div class="space-y-2 text-sm text-content-muted">
<p><strong>OPDS:</strong> { systemConfig["base_url"] }/opds</p> <p><strong class="text-content">OPDS:</strong> { systemConfig["base_url"] }/opds</p>
<p><strong>API:</strong> { systemConfig["base_url"] }/api</p> <p><strong class="text-content">API:</strong> { systemConfig["base_url"] }/api</p>
<p><strong>Device Sync:</strong> { systemConfig["base_url"] }/api/sync</p> <p><strong class="text-content">Device Sync:</strong> { systemConfig["base_url"] }/api/sync</p>
</div> </div>
</div> </div>
</div> </div>
+70 -62
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 min-h-screen bg-surface\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,322 +45,330 @@ 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-6 sm: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 btn-secondary font-medium\">")
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><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">System Settings</h1><p class=\"text-content-muted\">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, 5, "<div class=\"mb-6 p-4 rounded-lg border border-line bg-surface-raised text-danger\">")
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: 30, Col: 21}
} }
_, 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, 6, "</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, 7, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6\"><h3 class=\"text-xl font-semibold mb-6 text-content\">Base URL</h3><div><label class=\"block text-sm font-medium mb-2 text-content\">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: 41, Col: 41}
} }
_, 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, 8, "\" placeholder=\"https://books.example.com\" class=\"input\" required><p class=\"text-sm mt-1 text-content-muted\">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 font-medium\">Save Settings</button></div></div><div class=\"mt-8 card p-6\"><h3 class=\"text-xl font-semibold mb-6 text-content\">System Defaults</h3><div><label class=\"block text-sm font-medium mb-2 text-content\">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") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " 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, 9, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">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"] == "Pacific/Honolulu" { if systemConfig["default_timezone"] == "Pacific/Honolulu" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " 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, 11, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">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/Anchorage" { if systemConfig["default_timezone"] == "America/Anchorage" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " 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, 13, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">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/Los_Angeles" { 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, 15, " 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, 16, ">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/Denver" { if systemConfig["default_timezone"] == "America/Denver" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " 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, 18, ">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/Phoenix" { if systemConfig["default_timezone"] == "America/Phoenix" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " 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, 20, ">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"] == "America/Chicago" { if systemConfig["default_timezone"] == "America/Chicago" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " 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, 22, ">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"] == "America/New_York" { if systemConfig["default_timezone"] == "America/New_York" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " 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, 24, ">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"] == "America/Sao_Paulo" { if systemConfig["default_timezone"] == "America/Sao_Paulo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " 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, 26, ">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/London" { if systemConfig["default_timezone"] == "Europe/London" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " 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, 28, ">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"] == "Europe/Paris" { if systemConfig["default_timezone"] == "Europe/Paris" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " 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, 29, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">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"] == "Europe/Helsinki" { if systemConfig["default_timezone"] == "Europe/Helsinki" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " 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, 31, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">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"] == "Europe/Moscow" { if systemConfig["default_timezone"] == "Europe/Moscow" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " 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, 33, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ">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/Tehran" { if systemConfig["default_timezone"] == "Asia/Tehran" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " 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, 36, ">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/Dubai" { if systemConfig["default_timezone"] == "Asia/Dubai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " 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, 38, ">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/Karachi" { if systemConfig["default_timezone"] == "Asia/Karachi" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " 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, 40, ">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/Kolkata" { if systemConfig["default_timezone"] == "Asia/Kolkata" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " 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, 42, ">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/Dhaka" { if systemConfig["default_timezone"] == "Asia/Dhaka" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " 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, 44, ">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"] == "Asia/Bangkok" { if systemConfig["default_timezone"] == "Asia/Bangkok" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, " 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, 46, ">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"] == "Asia/Shanghai" { if systemConfig["default_timezone"] == "Asia/Shanghai" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " 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, 48, ">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"] == "Asia/Tokyo" { if systemConfig["default_timezone"] == "Asia/Tokyo" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " 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, 50, ">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 systemConfig["default_timezone"] == "Australia/Darwin" { if systemConfig["default_timezone"] == "Australia/Darwin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " 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, 52, ">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 systemConfig["default_timezone"] == "Australia/Sydney" { if systemConfig["default_timezone"] == "Australia/Sydney" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " 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, 54, ">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 systemConfig["default_timezone"] == "Pacific/Auckland" { if systemConfig["default_timezone"] == "Pacific/Auckland" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " 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, 56, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-1 text-content-muted\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-8 card p-6\"><h3 class=\"text-xl font-semibold mb-4 text-content\">URL Paths</h3><div class=\"space-y-2 text-sm text-content-muted\"><p><strong class=\"text-content\">OPDS:</strong> ")
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: 95, 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, 56, "/opds</p><p><strong>API:</strong> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "/opds</p><p><strong class=\"text-content\">API:</strong> ")
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: 96, Col: 79}
} }
_, 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, 58, "/api</p><p><strong class=\"text-content\">Device Sync:</strong> ")
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: 97, Col: 87}
} }
_, 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, 59, "/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
} }
+19 -12
View File
@@ -1,21 +1,28 @@
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-60 shrink-0 border-r border-line bg-surface-raised">
<div class="p-6"> <div class="p-5">
<h2 class="text-lg font-semibold mb-6" style="color: var(--text-primary)">Admin Panel</h2> <h2 class="mb-5 flex items-center gap-2 text-sm font-bold uppercase tracking-wide text-content-muted">
<nav class="space-y-2"> @Icon("shield", "h-4 w-4")
<a href="/admin" class={activeClass(currentPath, "/admin")} style="color: var(--text-primary)"> Admin
🏠 Dashboard </h2>
<nav class="space-y-1">
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
@Icon("home", "h-4 w-4")
Dashboard
</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-4 w-4")
Users
</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-4 w-4")
Libraries
</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-4 w-4")
Settings
</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-60 shrink-0 border-r border-line bg-surface-raised\"><div class=\"p-5\"><h2 class=\"mb-5 flex items-center gap-2 text-sm font-bold uppercase tracking-wide text-content-muted\">")
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, "Admin</h2><nav class=\"space-y-1\">")
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("home", "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, "Dashboard</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-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, "Users</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-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, "Libraries</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-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, "Settings</a></nav></div></aside>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+42 -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 min-h-screen bg-surface\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,7 +45,47 @@ 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-6 sm:p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">Dashboard</h1><p class=\"text-content-muted\">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\"><div class=\"flex items-center space-x-3\"><div class=\"text-content-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("library", "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, 4, "</div><div><h3 class=\"font-semibold text-content\">Library</h3><p class=\"text-content-muted text-sm\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"btn btn-secondary mt-4 text-sm\">View Library</a></div><div class=\"card p-6\"><div class=\"flex items-center space-x-3\"><div class=\"text-content-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "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><div><h3 class=\"font-semibold text-content\">Scan Watch Status</h3><p class=\"text-content-muted text-sm\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm text-content-muted\"><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\"><h3 class=\"text-xl font-semibold mb-4 text-content\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn btn-primary p-4 text-left\"><div class=\"font-medium\">Rescan Library</div><div class=\"text-content-muted text-sm\">Re-scan existing files and fix metadata</div></button> <a href=\"/admin/library\" class=\"btn btn-secondary p-4 text-left block\"><div class=\"font-medium\">Manage Libraries and Folders</div><div class=\"text-content-muted text-sm\">Add or remove libraries and scan directories</div></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"card mt-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 text-content flex items-center gap-2\">")
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, "Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"p-2 hover:bg-surface-hover rounded\">")
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, 7, "</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span class=\"text-content-muted\">Overall Progress</span> <span id=\"scan-progress-text\" class=\"text-content\">0%</span></div><div class=\"w-full bg-surface-hover rounded-full h-3\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500 bg-brand\" style=\"width: 0%;\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2 text-content-muted\">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 border-line bg-surface\"><h4 class=\"font-semibold mb-2 text-content flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "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, "Scan Complete!</h4><div id=\"scan-results-content\" class=\"text-content-muted\"><!-- 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
} }
+22 -27
View File
@@ -13,42 +13,42 @@ 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 min-h-screen bg-surface">
@AdminSidebar(currentUser, "/admin/users") @AdminSidebar(currentUser, "/admin/users")
<main class="flex-1 p-8"> <main class="flex-1 p-6 sm:p-8">
<div class="w-full"> <div class="w-full">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">User Management</h1>
<p style="color: var(--text-secondary)">Manage user accounts and permissions</p> <p class="text-content-muted">Manage user accounts and permissions</p>
</div> </div>
<!-- Users Table --> <!-- Users Table -->
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="card overflow-hidden">
<table class="w-full"> <table class="w-full">
<thead style="background-color: var(--bg-primary)"> <thead class="bg-surface">
<tr> <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-semibold uppercase tracking-wide text-content-muted">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-semibold uppercase tracking-wide text-content-muted">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-semibold uppercase tracking-wide text-content-muted">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-left text-xs font-semibold uppercase tracking-wide text-content-muted">Created</th>
<th class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider" style="color: var(--text-secondary)">Actions</th> <th class="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide text-content-muted">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y" style="divide-color: var(--border)"> <tbody class="divide-y divide-line">
for _, user := range users { for _, user := range users {
<tr id={ "user-" + user.ID } class="hover:bg-opacity-50" style="transition: background-color 0.2s;"> <tr id={ "user-" + user.ID } class="hover:bg-surface-hover" style="transition: background-color 0.2s;">
<!-- Username --> <!-- Username -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div> <div class="text-sm font-medium text-content">{ user.Username }</div>
if user.ID == currentUser.ID { if user.ID == currentUser.ID {
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span> <span class="badge bg-brand text-white">You</span>
} }
</div> </div>
</div> </div>
</td> </td>
<!-- Email --> <!-- Email -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-primary)">{ user.Email }</div> <div class="text-sm text-content">{ user.Email }</div>
</td> </td>
<!-- Role Toggle (with last-admin protection) --> <!-- Role Toggle (with last-admin protection) -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
@@ -57,8 +57,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
<div class="relative"> <div class="relative">
<select <select
disabled disabled
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50" class="input text-sm 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" title="Cannot demote the last admin"
> >
<option value="user">User</option> <option value="user">User</option>
@@ -72,8 +71,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
hx-target={ "#role-result-" + user.ID } hx-target={ "#role-result-" + user.ID }
hx-swap="innerHTML" hx-swap="innerHTML"
name="role" name="role"
class="text-sm rounded px-2 py-1" class="input text-sm"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
onchange="this.dispatchEvent(new Event('htmx:trigger'))" onchange="this.dispatchEvent(new Event('htmx:trigger'))"
hx-trigger="change" hx-trigger="change"
hx-vals='{"role": this.value}' hx-vals='{"role": this.value}'
@@ -86,7 +84,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
</td> </td>
<!-- Created --> <!-- Created -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div> <div class="text-sm text-content-muted">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
</td> </td>
<!-- Actions --> <!-- Actions -->
<td class="px-6 py-4 whitespace-nowrap text-right"> <td class="px-6 py-4 whitespace-nowrap text-right">
@@ -94,16 +92,14 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
hx-get={ "/admin/users/" + user.ID + "/profile-modal" } hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
hx-target="#modal-container" hx-target="#modal-container"
hx-swap="innerHTML" hx-swap="innerHTML"
class="text-sm px-3 py-1 rounded mr-2" class="btn btn-primary text-sm mr-2"
style="background-color: var(--accent); color: white;"
> >
Edit Edit
</button> </button>
if user.Role == "admin" && adminCount == 1 { if user.Role == "admin" && adminCount == 1 {
<button <button
disabled disabled
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50" class="btn btn-danger text-sm cursor-not-allowed opacity-50"
style="background-color: #dc2626; color: white;"
title="Cannot delete the last admin" title="Cannot delete the last admin"
> >
Delete Delete
@@ -115,8 +111,7 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
hx-target={ "#user-" + user.ID } hx-target={ "#user-" + user.ID }
hx-swap="outerHTML swap:0.5s" hx-swap="outerHTML swap:0.5s"
hx-confirm="Are you sure you want to delete this user? This action cannot be undone." hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
class="text-sm px-3 py-1 rounded" class="btn btn-danger text-sm"
style="background-color: #dc2626; color: white;"
> >
Delete Delete
</button> </button>
+21 -21
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 min-h-screen bg-surface\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,7 +45,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, 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-6 sm:p-8\"><div class=\"w-full\"><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">User Management</h1><p class=\"text-content-muted\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card overflow-hidden\"><table class=\"w-full\"><thead class=\"bg-surface\"><tr><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide text-content-muted\">Username</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide text-content-muted\">Email</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide text-content-muted\">Role</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide text-content-muted\">Created</th><th class=\"px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide text-content-muted\">Actions</th></tr></thead> <tbody class=\"divide-y divide-line\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -57,20 +57,20 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 37, Col: 34}
} }
_, 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, 5, "\" class=\"hover:bg-surface-hover\" 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 text-content\">")
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: 42, Col: 73}
} }
_, 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 {
@@ -81,19 +81,19 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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, 7, "<span class=\"badge bg-brand text-white\">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, 8, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm text-content\">")
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: 51, Col: 56}
} }
_, 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 {
@@ -104,7 +104,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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, 10, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"input text-sm cursor-not-allowed opacity-50\" 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
} }
@@ -116,7 +116,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 69, Col: 51}
} }
_, 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 {
@@ -129,13 +129,13 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 71, Col: 49}
} }
_, 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, 13, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"input text-sm\" 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
} }
@@ -162,7 +162,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 82, Col: 45}
} }
_, 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 {
@@ -173,14 +173,14 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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, 19, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm text-content-muted\">")
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: 87, Col: 106}
} }
_, 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 {
@@ -193,18 +193,18 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 92, Col: 64}
} }
_, 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, 21, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-primary text-sm mr-2\">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, 22, "<button disabled class=\"btn btn-danger text-sm cursor-not-allowed opacity-50\" title=\"Cannot delete the last admin\">Delete</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -216,7 +216,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 109, Col: 54}
} }
_, 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 {
@@ -229,13 +229,13 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
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: 111, Col: 42}
} }
_, 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, 25, "\" 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-sm\">Delete</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+30 -29
View File
@@ -14,78 +14,79 @@ templ Analytics(user User) {
@Header(user, "/analytics") @Header(user, "/analytics")
<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">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">📊 Reading Analytics</h1> <h1 class="mb-2 flex items-center gap-2 text-2xl font-bold tracking-tight text-content">
<p style="color: var(--text-secondary)">Track your reading habits and device usage</p> @Icon("chart", "h-6 w-6")
Reading Analytics
</h1>
<p class="text-content-muted">Track your reading habits and device usage</p>
</div> </div>
<!-- Date Range Picker --> <!-- 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="card p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center"> <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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 class="flex items-end">
<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 --> <!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8" id="stats-container"> <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);"> <div class="card p-5">
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-secondary);">Books Read</h3> <h3 class="text-xs font-semibold uppercase tracking-wide text-content-muted">Books Read</h3>
<p id="total-books" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="total-books" class="mt-1 text-2xl font-bold text-content">-</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="card p-5">
<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 text-content-muted">Pages Read</h3>
<p id="total-pages" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="total-pages" class="mt-1 text-2xl font-bold text-content">-</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="card p-5">
<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 text-content-muted">Reading Time</h3>
<p id="reading-time" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="reading-time" class="mt-1 text-2xl font-bold text-content">-</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="card p-5">
<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 text-content-muted">Completion Rate</h3>
<p id="completion-rate" class="text-3xl font-bold" style="color: var(--text-primary);">-</p> <p id="completion-rate" class="mt-1 text-2xl font-bold text-content">-</p>
</div> </div>
</div> </div>
<!-- Charts Row --> <!-- 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 --> <!-- Daily Reading Chart -->
<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-lg font-semibold mb-4" style="color: var(--text-primary);">Daily Reading Minutes</h3> <h3 class="text-lg font-semibold mb-4 text-content">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 --> <!-- Device Usage Chart -->
<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-lg font-semibold mb-4" style="color: var(--text-primary);">Device Usage</h3> <h3 class="text-lg font-semibold mb-4 text-content">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 --> <!-- Popular Books -->
<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-lg font-semibold mb-4" style="color: var(--text-primary);">Most Read Books</h3> <h3 class="text-lg font-semibold mb-4 text-content">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 text-content-muted">
<div class="loading-spinner mx-auto mb-4"></div> <div class="loading-spinner mx-auto mb-4"></div>
<p>Loading analytics...</p> <p>Loading analytics...</p>
</div> </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=\"w-full px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8\"><h1 class=\"mb-2 flex items-center gap-2 text-2xl font-bold tracking-tight text-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chart", "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, 3, "Reading Analytics</h1><p class=\"text-content-muted\">Track your reading habits and device usage</p></div><!-- Date Range Picker --><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-center\"><div class=\"flex-1 min-w-[200px]\"><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Start Date</label> <input type=\"date\" id=\"start-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div class=\"flex-1 min-w-[200px]\"><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">End Date</label> <input type=\"date\" id=\"end-date\" onchange=\"loadAnalytics()\" class=\"input\"></div><div class=\"flex items-end\"><button @click=\"loadAnalytics()\" class=\"btn btn-primary\">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-5\"><h3 class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Books Read</h3><p id=\"total-books\" class=\"mt-1 text-2xl font-bold text-content\">-</p></div><div class=\"card p-5\"><h3 class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Pages Read</h3><p id=\"total-pages\" class=\"mt-1 text-2xl font-bold text-content\">-</p></div><div class=\"card p-5\"><h3 class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Reading Time</h3><p id=\"reading-time\" class=\"mt-1 text-2xl font-bold text-content\">-</p></div><div class=\"card p-5\"><h3 class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Completion Rate</h3><p id=\"completion-rate\" class=\"mt-1 text-2xl font-bold text-content\">-</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\"><h3 class=\"text-lg font-semibold mb-4 text-content\">Daily Reading Minutes</h3><div class=\"h-80\"><canvas id=\"daily-reading-chart\"></canvas></div></div><!-- Device Usage Chart --><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4 text-content\">Device Usage</h3><div class=\"h-80\"><canvas id=\"device-usage-chart\"></canvas></div></div></div><!-- Popular Books --><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4 text-content\">Most Read Books</h3><div id=\"popular-books\" class=\"space-y-3\"><div class=\"text-center py-8 text-content-muted\"><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
} }
+22 -22
View File
@@ -3,63 +3,63 @@ package templates
templ APIExplorer(explorer APIExplorerData) { templ APIExplorer(explorer APIExplorerData) {
if !explorer.IsLoggedIn { if !explorer.IsLoggedIn {
// Show mode toggle and mock data // Show mode toggle and mock data
<div x-data="devices" class="border border-border rounded-lg p-6 mt-8 bg-background-secondary"> <div x-data="devices" class="card p-6 mt-8">
<div class="flex gap-2 mb-4"> <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="btn btn-primary">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> <button class="btn btn-secondary opacity-50 cursor-not-allowed" disabled>Login to Try Real</button>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<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"> <select disabled class="input w-full">
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option> <option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option> <option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option> <option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option> <option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
</select> </select>
</div> </div>
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body (Example)</h4> <h4 class="text-content-muted 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> <pre class="bg-surface p-4 rounded-lg overflow-x-auto"><code class="text-content text-sm">{ explorer.Endpoint.RequestBody }</code></pre>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<h4 class="text-text-secondary mb-2 text-sm font-medium">Response (Mock)</h4> <h4 class="text-content-muted 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">{ explorer.Endpoint.Response }</code></pre> <pre class="bg-surface p-4 rounded-lg overflow-x-auto"><code class="text-content text-sm">{ explorer.Endpoint.Response }</code></pre>
</div> </div>
<div class="flex gap-2 mt-4"> <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.RequestBody }, null, 2))" class="btn btn-secondary 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> <button @click="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))" class="btn btn-secondary text-sm">Copy Response</button>
</div> </div>
</div> </div>
} else { } else {
// Show interactive explorer with real execution // Show interactive explorer with real execution
<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="card p-6 mt-8" x-data="apiExplorerDoc" x-init="initAPIExplorerDoc('{ explorer.Endpoint.Path }', '{ explorer.Endpoint.RequestBody }', '{ explorer.Endpoint.Response }')">
<div class="flex gap-2 mb-4"> <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 btn btn-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> <button class="mode-btn btn btn-primary text-sm" id="real-btn" @click="showDocMode('real')">Try It Out</button>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<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"> <select id="http-method" class="input w-full">
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option> <option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option> <option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option> <option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option> <option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
</select> </select>
</div> </div>
<h4 class="text-text-secondary mb-2 text-sm font-medium">Request Body</h4> <h4 class="text-content-muted 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">{ explorer.Endpoint.RequestBody }</textarea> <textarea id="request-body" placeholder="Edit request body..." class="input w-full min-h-[150px] font-mono text-sm">{ 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> <button id="try-it-out" @click="tryDocEndpoint()" class="btn btn-primary mt-4">Try It Out</button>
<div class="api-response mt-4 hidden"> <div class="api-response mt-4 hidden">
<div class="flex justify-between mb-2 text-sm"> <div class="flex justify-between mb-2 text-sm">
<span id="response-status" class="font-semibold"></span> <span id="response-status" class="font-semibold"></span>
<span id="response-time" class="text-text-secondary"></span> <span id="response-time" class="text-content-muted"></span>
</div> </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> <pre class="bg-surface p-4 rounded-lg overflow-x-auto"><code id="response-body" class="text-content text-sm"></code></pre>
</div> </div>
<div class="flex gap-2 mt-4 flex-wrap"> <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="copyDocRequest()" class="btn btn-secondary 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="copyDocResponse()" class="btn btn-secondary 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> <button @click="generateDocCURL()" class="btn btn-secondary text-sm">Generate cURL</button>
</div> </div>
</div> </div>
} }
+10 -10
View File
@@ -30,7 +30,7 @@ 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\"><div class=\"flex gap-2 mb-4\"><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 class=\"mb-4\"><div class=\"mb-4\"><select disabled class=\"input w-full\"><option value=\"GET\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -70,38 +70,38 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
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 = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">DELETE</option></select></div><h4 class=\"text-content-muted mb-2 text-sm font-medium\">Request Body (Example)</h4><pre class=\"bg-surface p-4 rounded-lg overflow-x-auto\"><code class=\"text-content text-sm\">")
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(explorer.Endpoint.RequestBody) templ_7745c5c3_Var2, 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: 21, Col: 141} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 21, Col: 125}
} }
_, 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, 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\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</code></pre></div><div class=\"mt-4\"><h4 class=\"text-content-muted mb-2 text-sm font-medium\">Response (Mock)</h4><pre class=\"bg-surface p-4 rounded-lg overflow-x-auto\"><code class=\"text-content text-sm\">")
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(explorer.Endpoint.Response) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 25, Col: 138} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 25, Col: 122}
} }
_, 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, 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 = 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=\"btn btn-secondary text-sm\">Copy Request</button> <button @click=\"copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))\" class=\"btn btn-secondary text-sm\">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, 12, " <div class=\"card p-6 mt-8\" 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 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 class=\"mb-4\"><div class=\"mb-4\"><select id=\"http-method\" class=\"input w-full\"><option value=\"GET\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -141,20 +141,20 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
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, 20, ">DELETE</option></select></div><h4 class=\"text-content-muted mb-2 text-sm font-medium\">Request Body</h4><textarea id=\"request-body\" placeholder=\"Edit request body...\" class=\"input w-full min-h-[150px] font-mono text-sm\">")
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(explorer.Endpoint.RequestBody) templ_7745c5c3_Var4, 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: 49, Col: 151}
} }
_, 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, 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, 21, "</textarea></div><button id=\"try-it-out\" @click=\"tryDocEndpoint()\" class=\"btn btn-primary mt-4\">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-content-muted\"></span></div><pre class=\"bg-surface p-4 rounded-lg overflow-x-auto\"><code id=\"response-body\" class=\"text-content text-sm\"></code></pre></div><div class=\"flex gap-2 mt-4 flex-wrap\"><button @click=\"copyDocRequest()\" class=\"btn btn-secondary text-sm\">Copy Request</button> <button @click=\"copyDocResponse()\" class=\"btn btn-secondary text-sm\">Copy Response</button> <button @click=\"generateDocCURL()\" class=\"btn btn-secondary text-sm\">Generate cURL</button></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+140 -192
View File
@@ -9,65 +9,61 @@ 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"
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 p-6 w-full max-w-4xl mx-4 my-8"
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">
<div> <div>
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Progress</h2> <h2 class="text-2xl font-bold tracking-tight text-content">Sync Progress</h2>
<p class="text-sm" style="color: var(--text-secondary);">{ book.Title }</p> <p class="text-sm text-content-muted">{ book.Title }</p>
</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);"
> >
@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-lg border border-warning bg-warning/10"
style="background-color: #f59e0b20; border-color: #f59e0b;"
> >
<p class="font-semibold mb-2" style="color: var(--text-primary);"> <p class="font-semibold mb-2 flex items-center gap-2 text-content">
⚠️ Conflict Detected @Icon("alert", "h-4 w-4 text-warning")
Conflict Detected
</p> </p>
<p class="text-sm" style="color: var(--text-secondary);"> <p class="text-sm text-content-muted">
Progress differs between devices. Choose which version to keep. Progress differs between devices. Choose which version to keep.
</p> </p>
</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 rounded-lg border" class="card p-4"
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="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2" class="inline-block px-2 py-1 rounded text-xs font-semibold capitalize mb-2 bg-brand text-white"
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 text-content-muted">
{ FormatInTimezone(data.Timestamp, user.Timezone) } { FormatInTimezone(data.Timestamp, user.Timezone) }
</p> </p>
</div> </div>
<div class="text-right"> <div class="text-right">
if data.Data["percentage"] != nil { if data.Data["percentage"] != nil {
<div class="text-3xl font-bold" style="color: var(--accent);"> <div class="text-3xl font-bold text-brand">
{ fmt.Sprintf("%.1f", data.Data["percentage"].(float64) * 100) }% { fmt.Sprintf("%.1f", data.Data["percentage"].(float64) * 100) }%
</div> </div>
} }
</div> </div>
</div> </div>
if data.Data["page"] != nil { if data.Data["page"] != nil {
<div class="text-sm" style="color: var(--text-secondary);"> <div class="text-sm text-content-muted">
if data.Data["total_pages"] != nil { if data.Data["total_pages"] != nil {
Page: { fmt.Sprintf("%.0f", data.Data["page"].(float64)) } / { fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)) } Page: { fmt.Sprintf("%.0f", data.Data["page"].(float64)) } / { fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)) }
} else { } else {
@@ -76,15 +72,14 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
</div> </div>
} }
if data.Data["epubcfi"] != nil { if data.Data["epubcfi"] != nil {
<div class="text-xs mt-1" style="color: var(--text-secondary);"> <div class="text-xs mt-1 text-content-muted">
CFI: { data.Data["epubcfi"].(string) } CFI: { data.Data["epubcfi"].(string) }
</div> </div>
} }
<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 text-sm"
style="background-color: var(--accent); color: white;"
> >
Keep This Keep This
</button> </button>
@@ -93,26 +88,25 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
} }
</div> </div>
} else if book.ReadingProgress != nil { } else if book.ReadingProgress != nil {
<div class="mb-4" style="color: var(--text-secondary);"> <div class="mb-4 text-content-muted">
<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 rounded-lg border text-center" class="card p-6 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 text-brand">
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }% { fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64 * 100) }%
</div> </div>
<p class="capitalize text-lg mb-2" style="color: var(--text-primary);"> <p class="capitalize text-lg mb-2 text-content">
{ book.ReadingProgress.LastSyncSource.String } { book.ReadingProgress.LastSyncSource.String }
</p> </p>
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid { if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
<p style="color: var(--text-secondary);"> <p class="text-content-muted">
Page { book.ReadingProgress.CurrentPage.Int32 } of { book.ReadingProgress.TotalPages.Int32 } Page { book.ReadingProgress.CurrentPage.Int32 } of { book.ReadingProgress.TotalPages.Int32 }
</p> </p>
} }
if book.ReadingProgress.LastReadAt.Valid { if book.ReadingProgress.LastReadAt.Valid {
<p class="text-sm mt-2" style="color: var(--text-secondary);"> <p class="text-sm mt-2 text-content-muted">
Last read: { FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) } Last read: { FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }
</p> </p>
} }
@@ -121,8 +115,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>
@@ -135,23 +128,23 @@ 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"
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 p-8 w-full max-w-2xl mx-4 text-center"
style="background-color: var(--bg-secondary); border-color: var(--border);"
> >
<div class="text-6xl mb-4">📝</div> <div class="mb-4 flex justify-center">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Notes & Highlights</h2> @Icon("edit", "h-12 w-12 text-content-muted")
<p class="mb-2" style="color: var(--text-secondary);"> </div>
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Notes & Highlights</h2>
<p class="mb-2 text-content-muted">
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.
</p> </p>
<p class="mb-6" style="color: var(--text-secondary);">Feature coming soon!</p> <p class="mb-6 text-content-muted">Feature coming soon!</p>
<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>
@@ -164,27 +157,24 @@ 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"
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 mx-4 my-8 flex flex-col max-h-[90vh]"
style="background-color: var(--bg-secondary); border: 1px solid var(--border);"
> >
<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 border-line flex-shrink-0">
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit Metadata</h2> <h2 class="text-2xl font-bold tracking-tight text-content">Edit Metadata</h2>
<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);"
> >
@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) --> <!-- 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-lg overflow-hidden shadow-lg mb-3 cursor-pointer relative group bg-surface"
style="background-color: var(--bg-primary);"
@click="document.getElementById('cover-upload-input').click()" @click="document.getElementById('cover-upload-input').click()"
> >
if book.CoverImagePath.Valid && book.CoverImagePath.String != "" { if book.CoverImagePath.Valid && book.CoverImagePath.String != "" {
@@ -209,29 +199,29 @@ 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 text-sm"
style="background-color: var(--accent); color: white;"
@click="generateCover()" @click="generateCover()"
x-show="coverGenerating" x-show="coverGenerating"
x-cloak
disabled disabled
> >
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 text-sm"
style="background-color: var(--accent); color: white;"
@click="generateCover()" @click="generateCover()"
x-show="!coverGenerating" x-show="!coverGenerating"
x-cloak
> >
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-secondary w-full text-sm"
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"
x-cloak
> >
Remove Cover Remove Cover
</button> </button>
@@ -240,50 +230,44 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
<!-- Accordion Fields (Right) --> <!-- 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 --> <!-- Basic Info -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.basic ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">Title</label>
<input type="text" name="title" value={ book.Title } <input type="text" name="title" value={ book.Title }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Author</label>
<input type="text" name="author" value={ textToString(book.Author) } <input type="text" name="author" value={ textToString(book.Author) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Description</label>
<textarea name="description" rows="3" <textarea name="description" rows="3"
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Summary</label>
<textarea name="summary" rows="2" <textarea name="summary" rows="2"
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-brand text-white"
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:opacity-70 leading-none" @click="removeEditorTag(idx)">@Icon("close", "h-3 w-3")</button>
</span> </span>
</template> </template>
</div> </div>
@@ -297,13 +281,12 @@ 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 text-sm" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
<div <div
x-show="showTagDropdown" x-show="showTagDropdown"
x-cloak
x-transition x-transition
class="mt-1 w-full rounded-lg shadow-lg border max-h-48 overflow-y-auto" class="mt-1 w-full rounded-lg shadow-lg border border-line bg-surface max-h-48 overflow-y-auto"
style="background-color: var(--bg-primary); border-color: var(--border);"
> >
<template x-for="sug in tagSuggestions" :key="sug.value"> <template x-for="sug in tagSuggestions" :key="sug.value">
<button <button
@@ -314,171 +297,149 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
@click="selectEditorTagSuggestion(sug.value)" @click="selectEditorTagSuggestion(sug.value)"
> >
<span x-text="sug.value"></span> <span x-text="sug.value"></span>
<span class="text-xs" style="color: var(--text-secondary);" x-text="sug.count + ' books'"></span> <span class="text-xs text-content-muted" x-text="sug.count + ' books'"></span>
</button> </button>
</template> </template>
</div> </div>
</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 text-content-muted mb-1">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 text-sm" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Publication --> <!-- Publication -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.publication ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">Publisher</label>
<input type="text" name="publisher" value={ textToString(book.Publisher) } <input type="text" name="publisher" value={ textToString(book.Publisher) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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 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);">Edition</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Edition</label>
<input type="text" name="edition" value={ textToString(book.Edition) } <input type="text" name="edition" value={ textToString(book.Edition) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Language</label>
<input type="text" name="language" value={ textToString(book.Language) } <input type="text" name="language" value={ textToString(book.Language) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Genre</label>
<input type="text" name="genre" value={ textToString(book.Genre) } <input type="text" name="genre" value={ textToString(book.Genre) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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 text-sm" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Series --> <!-- Series -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.series ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">Series</label>
<input type="text" name="series" value={ textToString(book.Series) } <input type="text" name="series" value={ textToString(book.Series) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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 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);">Count</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 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);">Volume</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 text-sm" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Identifiers --> <!-- Identifiers -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.identifiers ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">ISBN</label>
<input type="text" name="isbn" value={ textToString(book.Isbn) } <input type="text" name="isbn" value={ textToString(book.Isbn) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">ASIN</label>
<input type="text" name="asin" value={ textToString(book.Asin) } <input type="text" name="asin" value={ textToString(book.Asin) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Goodreads ID</label>
<input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) } <input type="text" name="goodreads_id" value={ textToString(book.GoodreadsID) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">OpenLibrary ID</label>
<input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) } <input type="text" name="openlibrary_id" value={ textToString(book.OpenlibraryID) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Web URL</label>
<input type="url" name="web_url" value={ textToString(book.WebUrl) } <input type="url" name="web_url" value={ textToString(book.WebUrl) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input text-sm" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
<!-- Comic/Manga --> <!-- Comic/Manga -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.comic ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">Manga Type</label>
<select id="manga_type" name="manga_type" <select id="manga_type" name="manga_type"
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 +447,9 @@ 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 text-content-muted mb-1">Reading Direction</label>
<select id="reading_direction" name="reading_direction" <select id="reading_direction" name="reading_direction"
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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,96 +457,84 @@ 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 text-content-muted mb-1">Age Rating</label>
<input type="text" name="age_rating" value={ textToString(book.AgeRating) } <input type="text" name="age_rating" value={ textToString(book.AgeRating) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Story Arc</label>
<input type="text" name="story_arc" value={ textToString(book.StoryArc) } <input type="text" name="story_arc" value={ textToString(book.StoryArc) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Imprint</label>
<input type="text" name="imprint" value={ textToString(book.Imprint) } <input type="text" name="imprint" value={ textToString(book.Imprint) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Scan Information</label>
<input type="text" name="scan_information" value={ textToString(book.ScanInformation) } <input type="text" name="scan_information" value={ textToString(book.ScanInformation) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">Metadata Notes</label>
<textarea name="metadata_notes" rows="2" <textarea name="metadata_notes" rows="2"
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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">
<input type="checkbox" name="is_black_and_white" id="is_black_and_white" <input type="checkbox" name="is_black_and_white" id="is_black_and_white"
if book.IsBlackAndWhite.Bool { checked } if book.IsBlackAndWhite.Bool { checked }
class="rounded" /> class="rounded" />
<label for="is_black_and_white" class="text-sm" style="color: var(--text-secondary);">Black & White</label> <label for="is_black_and_white" class="text-sm text-content-muted">Black & White</label>
</div> </div>
</div> </div>
</div> </div>
<!-- Technical --> <!-- Technical -->
<div class="border rounded-lg" style="border-color: var(--border);"> <div class="border rounded-lg border-line">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center" <button type="button" class="w-full px-4 py-3 flex justify-between items-center text-content bg-surface"
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 x-text="openSections.technical ? '▾' : '▸'"></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-cloak x-transition class="p-4 space-y-3 bg-surface">
<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 text-content-muted mb-1">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 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);">Contributors (comma-separated)</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Contributors (comma-separated)</label>
<input type="text" name="contributors" value={ stringSliceToString(book.Contributors) } <input type="text" name="contributors" value={ stringSliceToString(book.Contributors) }
class="w-full px-3 py-2 rounded-lg border text-sm" class="input 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 text-content-muted mb-1">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 text-sm 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 text-content-muted mb-1">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 text-sm opacity-60" />
style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text-primary);" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<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 border-line flex-shrink-0">
<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;"
> >
Save Save
</button> </button>
File diff suppressed because it is too large Load Diff
+27 -33
View File
@@ -1,15 +1,17 @@
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" 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">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
if collection.ID != "" { if collection.ID != "" {
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Collection</h2> <h2 class="text-xl font-bold text-content">Edit Collection</h2>
} else { } else {
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Collection</h2> <h2 class="text-xl font-bold text-content">Create Collection</h2>
} }
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" @click="closeCollectionModal()" class="icon-btn">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
if collection.ID != "" { if collection.ID != "" {
<form <form
@@ -18,35 +20,32 @@ 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 text-content-muted mb-2">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 text-content-muted mb-2">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 text-content-muted mb-2">Icon</label>
<!-- Search/Text Input --> <!-- 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)"
@@ -57,14 +56,13 @@ templ CollectionModal(collection CollectionData) {
<!-- Emoji Grid --> <!-- 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 border-line bg-surface"
style="background-color: var(--bg-primary); border-color: var(--border);"
> >
<!-- Populated dynamically by JavaScript --> <!-- Populated dynamically by JavaScript -->
</div> </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 text-content-muted mb-2">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"></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"></button>
@@ -75,8 +73,8 @@ templ CollectionModal(collection CollectionData) {
<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,34 +83,31 @@ 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 text-content-muted mb-2">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 text-content-muted mb-2">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 text-content-muted mb-2">Icon</label>
<!-- Search/Text Input --> <!-- 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)"
@@ -123,14 +118,13 @@ templ CollectionModal(collection CollectionData) {
<!-- Emoji Grid --> <!-- 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 border-line bg-surface"
style="background-color: var(--bg-primary); border-color: var(--border);"
> >
<!-- Populated dynamically by JavaScript --> <!-- Populated dynamically by JavaScript -->
</div> </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 text-content-muted mb-2">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"></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"></button>
@@ -141,8 +135,8 @@ templ CollectionModal(collection CollectionData) {
<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>
} }
+28 -20
View File
@@ -29,66 +29,74 @@ 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\" 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\">")
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, 2, "<h2 class=\"text-xl font-bold text-content\">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, 3, "<h2 class=\"text-xl font-bold text-content\">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, 4, "<button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\">")
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>")
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, 6, "<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: 18, 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, 7, "\" 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: 21, 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, 8, "\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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: 27, 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, 9, "\" required class=\"input\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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
} }
@@ -101,56 +109,56 @@ func CollectionModal(collection CollectionData) 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, "</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, 10, "</textarea></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"input mb-2\" 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=\"")
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: 55, 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, 11, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border border-line bg-surface\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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=\"")
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: 73, 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, 12, "\"></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, 13, "<form hx-post=\"/api/collections\" hx-redirect=\"/collections\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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 text-content-muted mb-2\">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 text-content-muted mb-2\">Icon</label><!-- Search/Text Input --><input type=\"text\" id=\"icon-search\" class=\"input mb-2\" 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=\"")
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: 117, 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, 14, "\"><!-- Emoji Grid --><div id=\"icon-grid\" class=\"grid grid-cols-8 gap-1 max-h-32 overflow-y-auto p-2 rounded-lg border border-line bg-surface\"><!-- Populated dynamically by JavaScript --></div></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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 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, 15, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+46 -44
View File
@@ -14,39 +14,41 @@ 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> <div class="text-4xl" style="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 text-content">{ collection.Name }</h1>
<p style="color: var(--text-secondary)">Auto-Assign Rules</p> <p class="text-content-muted">Auto-Assign Rules</p>
</div> </div>
</div> </div>
</div> </div>
<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 tracking-tight text-content mb-4">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-6 text-center">
<div class="text-4xl mb-2">📋</div> <div class="mb-2 flex justify-center">
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3> @Icon("list", "h-10 w-10 text-content-muted")
<p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p> </div>
<h3 class="text-lg font-semibold mb-2 text-content">No Rules Yet</h3>
<p class="text-content-muted">Create auto-assign rules to automatically add books to this collection</p>
</div> </div>
</div> </div>
</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 tracking-tight text-content mb-4">Create New Rule</h2>
<form id="rule-form" @submit="handleCreateRule($event)"> <form id="rule-form" @submit="handleCreateRule($event)">
<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 mb-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 text-content-muted mb-2">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 text-content-muted mb-2">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>
@@ -79,13 +80,12 @@ templ CollectionRules(user User, collection CollectionData) {
</div> </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)">Value</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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>
@@ -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-content">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 text-content-muted">Uncheck to disable without deleting</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)">Priority</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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-content">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-content">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-content">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("play", "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-secondary">
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 mb-6">
<h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3> <h3 class="font-semibold mb-3 text-content">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 text-content-muted">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 text-content">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> <div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold bg-brand text-surface">1</div>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p> <p class="font-medium text-content">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 text-content-muted bg-surface-raised px-2 py-1 rounded">
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> <div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold bg-brand text-surface">2</div>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p> <p class="font-medium text-content">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 text-content-muted bg-surface-raised px-2 py-1 rounded">
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> <div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold bg-brand text-surface">3</div>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p> <p class="font-medium text-content">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 text-content-muted bg-surface-raised px-2 py-1 rounded">
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
+69 -70
View File
@@ -18,81 +18,85 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Conflicts</h1> <h1 class="text-2xl font-bold tracking-tight text-content">Sync Conflicts</h1>
<p style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p> <p class="text-content-muted">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 w-auto"
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 Dismiss All Resolved
</button> </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 flex space-x-6 text-sm">
<span style="color: var(--text-secondary)">Total: <strong style="color: var(--text-primary)">{ total }</strong></span> <span class="text-content-muted">Total: <strong class="text-content">{ total }</strong></span>
<span style="color: var(--text-secondary)">Unresolved: <strong style="color: #f59e0b;">{ unresolved }</strong></span> <span class="text-content-muted">Unresolved: <strong class="text-warning">{ unresolved }</strong></span>
<span style="color: var(--text-secondary)">Resolved: <strong style="color: #10b981;">{ total - unresolved }</strong></span> <span class="text-content-muted">Resolved: <strong class="text-success">{ total - unresolved }</strong></span>
</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">
<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 text-content">
<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 text-sm"
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 @Icon("x-circle", "h-4 w-4")
Dismiss Selected
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="loading" class="hidden text-center py-16" style="color: var(--text-secondary)"> <div id="loading" class="hidden text-center py-16 text-content-muted">
<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 text-center py-16 text-content-muted">
<div class="text-6xl mb-4"></div> <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-success">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3> @Icon("check-circle", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">No Conflicts</h3>
<p>Your devices are in sync! No conflicts to resolve.</p> <p>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-6">
<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()"/>
<span class="text-sm font-semibold" style="color: var(--text-primary);">Select All</span> <span class="text-sm font-semibold text-content">Select All</span>
</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="text-center py-16 text-content-muted">
<div class="text-6xl mb-4"></div> <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-success">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Conflicts</h3> @Icon("check-circle", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">No Conflicts</h3>
<p>Your devices are in sync! No conflicts to resolve.</p> <p>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 border-l-4 border-l-warning" data-conflict-id="{ conflict.ID }">
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4">
<div class="flex items-start space-x-4"> <div class="flex items-start space-x-4">
<input <input
@@ -102,15 +106,15 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
onchange="updateBulkActions()" onchange="updateBulkActions()"
/> />
<div> <div>
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3> <h3 class="text-lg font-semibold text-content">{ conflict.MediaItemTitle }</h3>
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p> <p class="text-sm text-content-muted">Type: { conflict.ConflictType }</p>
</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 Resolve
</button> </button>
</div> </div>
<div class="text-sm" style="color: var(--text-secondary)"> <div class="text-sm text-content-muted">
Created: { FormatInTimezone(conflict.CreatedAt, user.Timezone) } Created: { FormatInTimezone(conflict.CreatedAt, user.Timezone) }
if conflict.ResolvedBy != "" { if conflict.ResolvedBy != "" {
| Resolved by: { conflict.ResolvedBy } | Resolved by: { conflict.ResolvedBy }
@@ -119,94 +123,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" 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 mx-4 my-8">
<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-2xl font-bold tracking-tight text-content">Resolve Conflict</h2>
<p class="text-sm" id="conflict-book-title" style="color: var(--text-secondary);">Book Title</p> <p id="conflict-book-title" class="text-sm text-content-muted">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 bg-surface">
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Resolution Options</h3> <h3 class="font-semibold mb-4 text-content">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 p-3 border border-line rounded-lg cursor-pointer hover:bg-surface-hover">
<input type="radio" name="winner" value="koreader" class="mr-3" required/> <input type="radio" name="winner" value="koreader" class="mr-3" 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 text-content">Keep KOReader Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your KOReader device</p> <p class="text-xs text-content-muted">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 p-3 border border-line rounded-lg cursor-pointer hover:bg-surface-hover">
<input type="radio" name="winner" value="kobo" class="mr-3"/> <input type="radio" name="winner" value="kobo" class="mr-3"/>
<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 text-content">Keep Kobo Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your Kobo device</p> <p class="text-xs text-content-muted">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 p-3 border border-line rounded-lg cursor-pointer hover:bg-surface-hover">
<input type="radio" name="winner" value="web" class="mr-3"/> <input type="radio" name="winner" value="web" class="mr-3"/>
<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 text-content">Keep Web Progress</span>
<p class="text-xs" style="color: var(--text-secondary);">Use progress from the web interface</p> <p class="text-xs text-content-muted">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 p-3 border border-line rounded-lg cursor-pointer hover:bg-surface-hover">
<input type="radio" name="winner" value="manual" class="mr-3"/> <input type="radio" name="winner" value="manual" class="mr-3"/>
<div class="flex-1"> <div class="flex-1">
<span class="font-semibold" style="color: var(--text-primary);">Manual Override</span> <span class="font-semibold text-content">Manual Override</span>
<p class="text-xs" style="color: var(--text-secondary);">Specify a custom progress value</p> <p class="text-xs text-content-muted">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 border border-line rounded-lg bg-surface-raised">
<h4 class="font-semibold mb-3" style="color: var(--text-primary);">Manual Progress</h4> <h4 class="font-semibold mb-3 text-content">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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>
@@ -214,24 +214,23 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
<div class="mb-6"> <div class="mb-6">
<label class="flex items-center space-x-3 cursor-pointer"> <label class="flex items-center space-x-3 cursor-pointer">
<input type="checkbox" id="apply-to-all" class="w-5 h-5 rounded"/> <input type="checkbox" id="apply-to-all" class="w-5 h-5 rounded"/>
<span style="color: var(--text-primary);">Apply this resolution to all future conflicts</span> <span class="text-content">Apply this resolution to all future conflicts</span>
</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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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">
<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
+40 -50
View File
@@ -13,55 +13,51 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
<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="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> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">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> <p class="mb-6 text-content-muted">Build a custom dashboard section by defining filter rules or manually selecting books.</p>
<form id="custom-section-form" class="space-y-6"> <form id="custom-section-form" class="space-y-6">
<!-- Section Details --> <!-- Section Details -->
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <div class="p-4 rounded-lg bg-surface-raised">
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Section Details</h2> <h2 class="text-xl font-semibold mb-4 text-content">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 {
@@ -71,29 +67,28 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
</div> </div>
</div> </div>
<!-- Filter Rules --> <!-- Filter Rules -->
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <div class="p-4 rounded-lg bg-surface-raised">
<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-xl font-semibold text-content">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 text-content-muted">
Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule. Books matching these rules will be automatically added to your section. Use AND for all rules, OR for any rule.
</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 class="text-sm font-medium text-content-muted">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>
@@ -101,68 +96,64 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
</div> </div>
</div> </div>
<!-- Manual Book Selection --> <!-- Manual Book Selection -->
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <div class="p-4 rounded-lg bg-surface-raised">
<h2 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Manual Book Selection</h2> <h2 class="text-xl font-semibold mb-4 text-content">Manual Book Selection</h2>
<p class="text-sm mb-4" style="color: var(--text-secondary)"> <p class="text-sm mb-4 text-content-muted">
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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 p-3 rounded-lg max-h-60 overflow-y-auto bg-surface"
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 font-semibold uppercase tracking-wide text-content-muted mb-2">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-lg border-2 border-dashed border-line bg-surface"
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> <p class="text-sm text-center text-content-muted">No books selected</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Live Preview --> <!-- Live Preview -->
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary);"> <div class="p-4 rounded-lg bg-surface-raised">
<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-xl font-semibold text-content">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-primary"
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="p-4 rounded-lg bg-surface min-h-[200px]"
style="background-color: var(--bg-primary); min-height: 200px;"
> >
<p class="text-center" style="color: var(--text-secondary);"> <p class="text-center text-content-muted">
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>
@@ -172,17 +163,16 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
<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>
+37 -5
View File
@@ -37,7 +37,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, 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=\"max-w-4xl mx-auto px-4 py-8\"><h1 class=\"text-2xl font-bold tracking-tight text-content mb-2\">Create Custom Section</h1><p class=\"mb-6 text-content-muted\">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 bg-surface-raised\"><h2 class=\"text-xl font-semibold mb-4 text-content\">Section Details</h2><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Name *</label> <input type=\"text\" id=\"section-name\" name=\"name\" required class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Icon (emoji)</label> <input type=\"text\" id=\"section-icon\" name=\"icon\" maxlength=\"4\" class=\"input\" placeholder=\"📚\"></div></div><div class=\"mt-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Description</label> <textarea id=\"section-description\" name=\"description\" rows=\"2\" class=\"input\"></textarea></div><div class=\"mt-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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
} }
@@ -49,7 +49,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
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: 64, 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 {
@@ -62,7 +62,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
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: 64, 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 {
@@ -73,7 +73,39 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
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, 6, "</select></div></div><!-- Filter Rules --><div class=\"p-4 rounded-lg bg-surface-raised\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold text-content\">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, 7, "Add Rule</button></div><p class=\"text-sm mb-4 text-content-muted\">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 text-content-muted\">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><!-- Manual Book Selection --><div class=\"p-4 rounded-lg bg-surface-raised\"><h2 class=\"text-xl font-semibold mb-4 text-content\">Manual Book Selection</h2><p class=\"text-sm mb-4 text-content-muted\">Add specific books to this section. Use the search to find and select multiple books.</p><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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, 8, "Search</button></div></div><div id=\"search-results\" class=\"hidden mb-4 p-3 rounded-lg max-h-60 overflow-y-auto bg-surface\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Selected Books</label><div id=\"selected-books\" class=\"min-h-[60px] p-3 rounded-lg border-2 border-dashed border-line bg-surface\"><p class=\"text-sm text-center text-content-muted\">No books selected</p></div></div></div><!-- Live Preview --><div class=\"p-4 rounded-lg bg-surface-raised\"><div class=\"flex items-center justify-between mb-4\"><h2 class=\"text-xl font-semibold text-content\">Live Preview</h2><button type=\"button\" id=\"preview-btn\" 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, 9, "Refresh Preview</button></div><div id=\"preview-container\" class=\"p-4 rounded-lg bg-surface min-h-[200px]\"><p class=\"text-center text-content-muted\">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=\"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, 10, "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 +113,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, 11, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+133 -125
View File
@@ -19,11 +19,12 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1> <h1 class="text-2xl font-bold tracking-tight text-content">Device Management</h1>
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p> <p class="text-content-muted">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>
@@ -31,11 +32,13 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<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 --> <!-- 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="text-center py-16 col-span-full text-content-muted">
<div class="text-6xl mb-4">📱</div> <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3> @Icon("device", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">No Devices Yet</h3>
<p class="mb-4">Add your reading devices to enable cross-device sync</p> <p class="mb-4">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 Add Your First Device
</button> </button>
</div> </div>
@@ -44,7 +47,7 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
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-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
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"> <div class="text-4xl">
if device.DeviceType == "koreader" { if device.DeviceType == "koreader" {
@@ -58,103 +61,110 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
} }
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<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" aria-label="Shelf mappings">
📚 @Icon("library", "h-4 w-4")
</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" aria-label="Device settings">
⚙️ @Icon("settings", "h-4 w-4")
</button> </button>
</div> </div>
</div> </div>
<h3 class="text-lg font-semibold mb-1" style="color: var(--text-primary)">{ device.DeviceName }</h3> <h3 class="text-lg font-semibold mb-1 text-content">{ device.DeviceName }</h3>
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</p> <p class="text-sm mb-4 text-content-muted">{ device.DeviceType }</p>
<div class="space-y-2 text-sm mb-4"> <div class="space-y-2 text-sm mb-4">
<div class="flex justify-between"> <div class="flex justify-between">
<span style="color: var(--text-secondary)">Sync Status</span> <span class="text-content-muted">Sync Status</span>
if device.SyncEnabled { if device.SyncEnabled {
<span style="color: var(--accent)"> Enabled</span> <span class="inline-flex items-center gap-1 text-brand">
@Icon("check", "h-3.5 w-3.5")
Enabled
</span>
} else { } else {
<span style="color: var(--text-secondary)"> Disabled</span> <span class="inline-flex items-center gap-1 text-content-muted">
@Icon("close", "h-3.5 w-3.5")
Disabled
</span>
} }
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span style="color: var(--text-secondary)">Last Sync</span> <span class="text-content-muted">Last Sync</span>
if device.LastSync != nil { if device.LastSync != nil {
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSync, user.Timezone) }</span> <span class="text-content">{ FormatInTimezone(*device.LastSync, user.Timezone) }</span>
} else { } else {
<span style="color: var(--text-primary)">Never</span> <span class="text-content">Never</span>
} }
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span style="color: var(--text-secondary)">Last Seen</span> <span class="text-content-muted">Last Seen</span>
if device.LastSeen != nil { if device.LastSeen != nil {
<span style="color: var(--text-primary)">{ FormatInTimezone(*device.LastSeen, user.Timezone) }</span> <span class="text-content">{ FormatInTimezone(*device.LastSeen, user.Timezone) }</span>
} else { } else {
<span style="color: var(--text-primary)">Never</span> <span class="text-content">Never</span>
} }
</div> </div>
</div> </div>
<!-- NEW: Sync URL & Token Management --> <!-- NEW: Sync URL & Token Management -->
<div class="border-t pt-4" style="border-color: var(--border);"> <div class="border-t border-line pt-4">
<p class="text-xs font-semibold mb-2" style="color: var(--text-secondary)">DEVICE SYNC CONFIGURATION</p> <p class="text-xs font-semibold mb-2 text-content-muted">DEVICE SYNC CONFIGURATION</p>
if device.DeviceType == "kobo" { if device.DeviceType == "kobo" {
<!-- Kobo: Copy Full Sync URL --> <!-- 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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Kobo Sync URL</label>
<div class="flex space-x-2"> <div class="flex space-x-2">
<input <input
type="text" type="text"
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 flex-1 text-xs"
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"
style="background-color: var(--accent); color: white;"
> >
📋 Copy @Icon("copy", "h-3.5 w-3.5")
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 text-content-muted">Paste this URL into Kobo's <code class="px-1 py-0.5 rounded bg-surface">api_endpoint</code> setting</p>
</div> </div>
} }
if device.DeviceType == "koreader" { if device.DeviceType == "koreader" {
<!-- KOReader: Copy Auth Token --> <!-- 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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Auth Token (for plugin)</label>
<div class="flex space-x-2"> <div class="flex space-x-2">
<input <input
type="text" type="text"
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 flex-1 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"
style="background-color: var(--accent); color: white;"
> >
📋 Copy @Icon("copy", "h-3.5 w-3.5")
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 text-content-muted">Enter this token in the KOReader plugin settings</p>
</div> </div>
} }
<!-- Regenerate Token Button --> <!-- 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-3.5 w-3.5")
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 inline-flex items-center gap-1 text-content-muted">
@Icon("alert", "h-3 w-3")
Old token will immediately stop working
</p>
</div> </div>
</div> </div>
} }
@@ -164,21 +174,21 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<!-- Pending Registrations --> <!-- 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-2xl font-bold tracking-tight mb-4 text-content">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">
<div> <div>
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3> <h3 class="font-semibold text-content">{ reg.DeviceName }</h3>
<p class="text-sm" style="color: var(--text-secondary)"> <p class="text-sm text-content-muted">
{ reg.DeviceType } - Expires in { reg.ExpiresAt } { reg.DeviceType } - Expires in { reg.ExpiresAt }
</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>
@@ -190,8 +200,8 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<!-- Sync Queue Section --> <!-- 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-2xl font-bold tracking-tight text-content">Sync Queue</h2>
<button @click="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg"> <button @click="clearSyncQueue()" class="btn btn-secondary">
Clear Queue Clear Queue
</button> </button>
</div> </div>
@@ -199,31 +209,31 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</div> </div>
</div> </div>
<!-- Add Device Modal --> <!-- Add Device Modal -->
<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 id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" 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">
<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 text-content">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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,22 +243,21 @@ 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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 text-content-muted">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 Cancel
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg"> <button type="submit" class="btn btn-primary">
Register Device Register Device
</button> </button>
</div> </div>
@@ -256,23 +265,24 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</div> </div>
</div> </div>
<!-- Device Settings Modal --> <!-- Device Settings Modal -->
<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 id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" 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">
<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 text-content">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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">
@@ -283,9 +293,9 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
checked checked
class="w-5 h-5 rounded" class="w-5 h-5 rounded"
/> />
<span style="color: var(--text-primary)">Enable Sync</span> <span class="text-content">Enable Sync</span>
</label> </label>
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Allow this device to sync reading progress</p> <p class="text-xs mt-1 ml-8 text-content-muted">Allow this device to sync reading progress</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="flex items-center space-x-3 cursor-pointer"> <label class="flex items-center space-x-3 cursor-pointer">
@@ -295,16 +305,15 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
checked checked
class="w-5 h-5 rounded" class="w-5 h-5 rounded"
/> />
<span style="color: var(--text-primary)">Auto Sync</span> <span class="text-content">Auto Sync</span>
</label> </label>
<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 text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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>
@@ -313,15 +322,14 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
<option value="60">Every hour</option> <option value="60">Every hour</option>
</select> </select>
</div> </div>
<div class="border-t pt-6 mb-6" style="border-color: var(--border);"> <div class="border-t border-line pt-6 mb-6">
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Collection View Settings</h3> <h3 class="text-lg font-semibold mb-4 text-content">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 text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 +337,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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 +349,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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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>
@@ -362,9 +368,9 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
checked checked
class="w-5 h-5 rounded" class="w-5 h-5 rounded"
/> />
<span style="color: var(--text-primary)">Show Cover Images</span> <span class="text-content">Show Cover Images</span>
</label> </label>
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display book covers in collection views</p> <p class="text-xs mt-1 ml-8 text-content-muted">Display book covers in collection views</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="flex items-center space-x-3 cursor-pointer"> <label class="flex items-center space-x-3 cursor-pointer">
@@ -373,19 +379,19 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
id="settings-show-progress" id="settings-show-progress"
class="w-5 h-5 rounded" class="w-5 h-5 rounded"
/> />
<span style="color: var(--text-primary)">Show Reading Progress</span> <span class="text-content">Show Reading Progress</span>
</label> </label>
<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 text-content-muted">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">
<button type="button" @click="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg"> <button type="button" @click="hideDeviceSettingsModal()" class="btn btn-secondary">
Cancel Cancel
</button> </button>
<button type="button" @click="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg"> <button type="button" @click="handleRevokeDevice()" class="btn btn-danger">
Revoke Device Revoke Device
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg"> <button type="submit" class="btn btn-primary">
Save Settings Save Settings
</button> </button>
</div> </div>
@@ -393,66 +399,68 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
</div> </div>
</div> </div>
<!-- Device Shelf Mappings Modal --> <!-- Device Shelf Mappings Modal -->
<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 id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: var(--surface-overlay);">
<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="card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
<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 text-content">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 class="text-content-muted">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 Close
</button> </button>
<button type="button" @click="showAddMappingModal()" class="btn-primary px-4 py-2 rounded-lg"> <button type="button" @click="showAddMappingModal()" class="btn btn-primary">
Add Mapping @Icon("plus", "h-4 w-4")
Add Mapping
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Add/Edit Shelf Mapping Modal --> <!-- Add/Edit Shelf Mapping Modal -->
<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 id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" 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">
<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 text-content">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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,10 +469,10 @@ 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 Cancel
</button> </button>
<button type="submit" class="btn-primary px-4 py-2 rounded-lg"> <button type="submit" class="btn btn-primary">
Save Mapping Save Mapping
</button> </button>
</div> </div>
File diff suppressed because one or more lines are too long
+17 -19
View File
@@ -12,27 +12,25 @@ 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 bg-surface text-content font-sans antialiased" }>
@Header(user, currentPath) @Header(user, currentPath)
<!-- Mobile Menu Button --> <!-- 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="icon-btn lg:hidden fixed top-4 left-4 z-50"
onclick="toggleSidebar()" onclick="toggleSidebar()"
aria-label="Toggle menu" aria-label="Toggle menu"
> >
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> @Icon("menu", "h-6 w-6")
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button> </button>
<!-- Search Results Overlay --> <!-- 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> <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 --> <!-- 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"> <div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-surface-raised border-r border-line overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out">
<!-- Search --> <!-- Search -->
<div class="p-4 border-b border-border"> <div class="p-4 border-b border-line">
<input <input
type="text" 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" class="search-input input text-sm"
placeholder="Search documentation..." placeholder="Search documentation..."
id="docs-search" id="docs-search"
/> />
@@ -41,7 +39,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
for _, section := range nav.Sections { for _, section := range nav.Sections {
<div class="nav-section mb-6"> <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" class="nav-section-title font-semibold text-xs uppercase tracking-wide text-content-muted px-4 py-3 cursor-pointer select-none flex items-center justify-between"
@click="toggleSection($el)" @click="toggleSection($el)"
> >
<span>{ section.Title }</span> <span>{ section.Title }</span>
@@ -52,7 +50,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
if section.Collapsed { if section.Collapsed {
<div class="nav-items hidden" data-collapsed="true"> <div class="nav-items hidden" data-collapsed="true">
for _, item := range section.Items { for _, item := range section.Items {
<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"> <a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-content-muted hover:text-brand text-sm transition-colors no-underline">
<span class="mr-2">{ item.Icon }</span>{ item.Title } <span class="mr-2">{ item.Icon }</span>{ item.Title }
</a> </a>
} }
@@ -60,7 +58,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
} else { } else {
<div class="nav-items" data-collapsed="false"> <div class="nav-items" data-collapsed="false">
for _, item := range section.Items { for _, item := range section.Items {
<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"> <a href={ item.URL } class="nav-item block py-2 px-4 pl-8 text-content-muted hover:text-brand text-sm transition-colors no-underline">
<span class="mr-2">{ item.Icon }</span>{ item.Title } <span class="mr-2">{ item.Icon }</span>{ item.Title }
</a> </a>
} }
@@ -73,28 +71,28 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
<main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto"> <main class="main-content lg:ml-72 p-8 max-w-4xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
if len(doc.Breadcrumb) > 0 { if len(doc.Breadcrumb) > 0 {
<nav class="breadcrumb flex gap-2 text-sm text-text-secondary mb-8" aria-label="Breadcrumb"> <nav class="breadcrumb flex gap-2 text-sm text-content-muted mb-8" aria-label="Breadcrumb">
for i, crumb := range doc.Breadcrumb { for i, crumb := range doc.Breadcrumb {
if i > 0 { if i > 0 {
<span class="text-text-secondary/50"></span> <span class="text-content-muted/50"></span>
} }
<a href={ crumb.URL } class="text-accent hover:underline no-underline">{ crumb.Title }</a> <a href={ crumb.URL } class="text-brand hover:underline no-underline">{ crumb.Title }</a>
} }
</nav> </nav>
} }
<!-- Title --> <!-- Title -->
<h1 class="text-4xl font-bold mb-8 text-text-primary">{ doc.Title }</h1> <h1 class="text-3xl font-bold tracking-tight mb-8 text-content">{ doc.Title }</h1>
<!-- Table of Contents --> <!-- Table of Contents -->
if len(doc.TOC) > 0 { if len(doc.TOC) > 0 {
<details class="toc bg-background-secondary p-4 rounded-lg mb-8 border border-border"> <details class="toc bg-surface-raised p-4 rounded-lg mb-8 border border-line">
<summary class="cursor-pointer hover:opacity-80 transition-opacity"> <summary class="cursor-pointer hover:opacity-80 transition-opacity">
<strong class="text-text-primary font-semibold">On this page</strong> <strong class="text-content font-semibold">On this page</strong>
<span class="ml-2 text-text-secondary text-xs"></span> <span class="ml-2 text-content-muted text-xs"></span>
</summary> </summary>
for _, item := range doc.TOC { for _, item := range doc.TOC {
<a <a
href={ "#" + item.Anchor } href={ "#" + item.Anchor }
class="toc-item block py-1 text-text-secondary hover:text-accent text-sm no-underline" class="toc-item block py-1 text-content-muted hover:text-brand text-sm no-underline"
style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) } style={ "margin-left: " + fmt.Sprintf("%drem", item.Level) }
> >
{ item.Title } { item.Title }
+58 -50
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 bg-surface text-content 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,133 +74,136 @@ 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, "<!-- Mobile Menu Button --><button class=\"icon-btn lg:hidden fixed top-4 left-4 z-50\" onclick=\"toggleSidebar()\" aria-label=\"Toggle menu\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("menu", "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, 6, "</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-surface-raised border-r border-line overflow-y-auto mt-16 lg:translate-x-0 transition-transform duration-300 ease-in-out\"><!-- Search --><div class=\"p-4 border-b border-line\"><input type=\"text\" class=\"search-input input text-sm\" placeholder=\"Search documentation...\" id=\"docs-search\"></div><!-- Navigation Sections -->")
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, 7, "<div class=\"nav-section mb-6\"><div class=\"nav-section-title font-semibold text-xs uppercase tracking-wide text-content-muted px-4 py-3 cursor-pointer select-none flex items-center justify-between\" @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: 45, Col: 28}
} }
_, 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, 8, "</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>")
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, 9, "<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, 10, "<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: 53, Col: 27}
} }
_, 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, 11, "\" class=\"nav-item block py-2 px-4 pl-8 text-content-muted hover:text-brand text-sm transition-colors no-underline\"><span class=\"mr-2\">")
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: 54, Col: 40}
} }
_, 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, 12, "</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: 54, Col: 61}
} }
_, 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, 13, "</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, 14, "</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, 15, "<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, 16, "<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: 61, Col: 27}
} }
_, 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, 17, "\" class=\"nav-item block py-2 px-4 pl-8 text-content-muted hover:text-brand text-sm transition-colors no-underline\"><span class=\"mr-2\">")
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: 62, Col: 40}
} }
_, 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, 18, "</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: 62, Col: 61}
} }
_, 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, 19, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -210,131 +213,136 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
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, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div><!-- Main Content --><main class=\"main-content lg:ml-72 p-8 max-w-4xl mx-auto\"><!-- Breadcrumb -->")
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, 23, "<nav class=\"breadcrumb flex gap-2 text-sm text-content-muted mb-8\" aria-label=\"Breadcrumb\">")
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 = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<span class=\"text-content-muted/50\"></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, 24, " <a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " <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: 79, Col: 26}
} }
_, 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, 26, "\" class=\"text-brand hover:underline no-underline\">")
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: 79, Col: 90}
} }
_, 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, 27, "</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, 28, "</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, 29, "<!-- Title --><h1 class=\"text-3xl font-bold tracking-tight mb-8 text-content\">")
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: 84, Col: 79}
} }
_, 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, 30, "</h1><!-- Table of Contents -->")
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, 31, "<details class=\"toc bg-surface-raised p-4 rounded-lg mb-8 border border-line\"><summary class=\"cursor-pointer hover:opacity-80 transition-opacity\"><strong class=\"text-content font-semibold\">On this page</strong> <span class=\"ml-2 text-content-muted text-xs\">▼</span></summary> ")
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, 32, "<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: 94, Col: 32}
} }
_, 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, 33, "\" class=\"toc-item block py-1 text-content-muted hover:text-brand text-sm no-underline\" 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))
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: 96, Col: 66}
} }
_, 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, 34, "\">")
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: 98, Col: 20}
} }
_, 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, 35, "</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, 36, "</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, 37, "<!-- Content --><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 +350,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, 38, "</div></main></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+4 -7
View File
@@ -3,26 +3,23 @@ package templates
// FilterItem renders a single saved filter item for the list // 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 p-2 rounded bg-surface 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 rounded text-content"
style="color: var(--text-primary);"
> >
{ name } { name }
</button> </button>
<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"
style="color: var(--text-secondary);"
title="Delete filter" title="Delete filter"
> >
🗑 @Icon("trash", "h-4 w-4")
</button> </button>
</div> </div>
} }
+13 -5
View File
@@ -30,33 +30,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 p-2 rounded bg-surface 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: 7, 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 rounded text-content\">")
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\" title=\"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
} }
+52 -74
View File
@@ -12,109 +12,87 @@ templ Index(loggedIn bool) {
<link href="/static/style.css" rel="stylesheet"/> <link href="/static/style.css" rel="stylesheet"/>
</head> </head>
<body class="theme-tokyo-night" x-data="index" x-init="initIndexTheme(); checkAuthRedirect()"> <body class="theme-tokyo-night" x-data="index" x-init="initIndexTheme(); checkAuthRedirect()">
<!-- Hero Section --> <!-- Hero -->
<div class="relative overflow-hidden"> <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="pointer-events-none absolute inset-0 bg-gradient-to-br from-brand/15 via-transparent to-brand/5"></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="relative mx-auto max-w-5xl px-4 py-20 sm:px-6 lg:px-8 lg:py-28">
<div class="flex justify-between items-start mb-8"> <div class="absolute right-4 top-4">
<div></div> <select id="theme-select" @change="changeTheme" class="input w-auto py-1.5 text-xs">
<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)"> for _, t := range ThemeOptions {
<option value="tokyo-night">Tokyo Night</option> <option value={ t.Name }>{ t.Label }</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> </select>
</div> </div>
<div class="text-center"> <div class="text-center">
<h1 class="text-4xl sm:text-6xl lg:text-7xl font-extrabold" style="color: var(--text-primary)"> <div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand/15 text-brand">
📚 Bookhoard @Icon("book-open", "h-9 w-9")
</div>
<h1 class="text-4xl font-extrabold tracking-tight text-content sm:text-6xl">
Your books, beautifully organized.
</h1> </h1>
<p class="mt-6 max-w-2xl mx-auto text-xl" style="color: var(--text-secondary)"> <p class="mx-auto mt-5 max-w-2xl text-lg text-content-muted">
Your personal ebook management system. Track reading progress, organize your library, and enjoy beautiful themes. A self-hosted ebook and comic library with cross-device sync, a built-in reader, and reading analytics.
</p> </p>
<div class="mt-10"> <div class="mt-8 flex justify-center">
<a href="#auth" class="btn-primary px-8 py-3 rounded-lg font-medium text-lg"> <a href="#auth" class="btn btn-primary px-8 py-3 text-base">
@Icon("arrow-right", "h-4 w-4")
Get Started Get Started
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Features Section -->
<div class="py-16" style="background-color: var(--bg-secondary)"> <!-- Features -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="border-t border-line bg-surface-raised">
<div class="text-center mb-12"> <div class="mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8">
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Why Bookhoard?</h2> <div class="mb-10 text-center">
<p class="mt-4 text-lg" style="color: var(--text-secondary)">Discover the features that make ebook management effortless</p> <h2 class="text-2xl font-bold tracking-tight text-content">Why Bookhoard?</h2>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div class="grid grid-cols-1 gap-5 md:grid-cols-3">
<div class="card p-6 rounded-xl border"> <div class="card p-6">
<div class="text-center"> <div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand">
<div class="mx-auto w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center mb-4"> @Icon("clock", "h-6 w-6")
<span class="text-white text-2xl">📖</span>
</div> </div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Reading Progress</h3> <h3 class="mb-1.5 text-base font-semibold text-content">Cross-device sync</h3>
<p style="color: var(--text-secondary)">Track your reading progress across all your ebooks with detailed statistics.</p> <p class="text-sm leading-relaxed text-content-muted">Sync reading progress, bookmarks, and highlights across KOReader, Kobo, and the web.</p>
</div> </div>
<div class="card p-6">
<div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand">
@Icon("book-open", "h-6 w-6")
</div> </div>
<div class="card p-6 rounded-xl border"> <h3 class="mb-1.5 text-base font-semibold text-content">Built-in reader</h3>
<div class="text-center"> <p class="text-sm leading-relaxed text-content-muted">Read EPUB, PDF, and comics in the browser with custom themes, fonts, and layouts.</p>
<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> </div>
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">Beautiful Themes</h3> <div class="card p-6">
<p style="color: var(--text-secondary)">Choose from 11 stunning themes including Tokyo Night, Dracula, and Catppuccin variants.</p> <div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand">
@Icon("chart", "h-6 w-6")
</div> </div>
</div> <h3 class="mb-1.5 text-base font-semibold text-content">Smart collections</h3>
<div class="card p-6 rounded-xl border"> <p class="text-sm leading-relaxed text-content-muted">Organize with rules-based collections, series tracking, and reading analytics.</p>
<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> </div>
</div> </div>
</div>
<!-- Auth Section --> <!-- Auth -->
if !loggedIn { if !loggedIn {
<div id="auth" class="py-16"> <div id="auth" class="mx-auto max-w-md px-4 py-16 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="card p-6">
<div class="text-center mb-12"> <h2 class="mb-5 text-center text-lg font-bold text-content">Sign in to Bookhoard</h2>
<h2 class="text-3xl font-extrabold" style="color: var(--text-primary)">Join Bookhoard Today</h2> <form hx-post="/api/auth/login" hx-target="#auth-result" hx-swap="innerHTML" class="space-y-4">
<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> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email or Username</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="text" name="login" class="input" required/>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="password" name="password" class="input" required/>
</div>
<button type="submit" class="w-full btn-primary py-2 rounded-lg font-medium">
Sign In
</button>
</div> </div>
<button type="submit" class="btn btn-primary w-full">Sign In</button>
</form> </form>
</div> </div>
</div> <div id="auth-result" class="mt-6 text-center"></div>
<div id="auth-result" class="mt-8 text-center"></div>
</div>
</div> </div>
} }
</body> </body>
+79 -3
View File
@@ -29,17 +29,93 @@ 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()\"><!-- Hero --><div class=\"relative overflow-hidden\"><div class=\"pointer-events-none absolute inset-0 bg-gradient-to-br from-brand/15 via-transparent to-brand/5\"></div><div class=\"relative mx-auto max-w-5xl px-4 py-20 sm:px-6 lg:px-8 lg:py-28\"><div class=\"absolute right-4 top-4\"><select id=\"theme-select\" @change=\"changeTheme\" class=\"input w-auto py-1.5 text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, t := 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(t.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 22, 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(t.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 22, Col: 42}
}
_, 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><div class=\"text-center\"><div class=\"mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand/15 text-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-9 w-9").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><h1 class=\"text-4xl font-extrabold tracking-tight text-content sm:text-6xl\">Your books, beautifully organized.</h1><p class=\"mx-auto mt-5 max-w-2xl text-lg text-content-muted\">A self-hosted ebook and comic library with cross-device sync, a built-in reader, and reading analytics.</p><div class=\"mt-8 flex justify-center\"><a href=\"#auth\" class=\"btn btn-primary px-8 py-3 text-base\">")
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, 7, "Get Started</a></div></div></div></div><!-- Features --><div class=\"border-t border-line bg-surface-raised\"><div class=\"mx-auto max-w-5xl px-4 py-16 sm:px-6 lg:px-8\"><div class=\"mb-10 text-center\"><h2 class=\"text-2xl font-bold tracking-tight text-content\">Why Bookhoard?</h2></div><div class=\"grid grid-cols-1 gap-5 md:grid-cols-3\"><div class=\"card p-6\"><div class=\"mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("clock", "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, 8, "</div><h3 class=\"mb-1.5 text-base font-semibold text-content\">Cross-device sync</h3><p class=\"text-sm leading-relaxed text-content-muted\">Sync reading progress, bookmarks, and highlights across KOReader, Kobo, and the web.</p></div><div class=\"card p-6\"><div class=\"mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand\">")
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=\"mb-1.5 text-base font-semibold text-content\">Built-in reader</h3><p class=\"text-sm leading-relaxed text-content-muted\">Read EPUB, PDF, and comics in the browser with custom themes, fonts, and layouts.</p></div><div class=\"card p-6\"><div class=\"mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand/15 text-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chart", "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=\"mb-1.5 text-base font-semibold text-content\">Smart collections</h3><p class=\"text-sm leading-relaxed text-content-muted\">Organize with rules-based collections, series tracking, and reading analytics.</p></div></div></div></div><!-- Auth -->")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if !loggedIn { if !loggedIn {
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, 11, "<div id=\"auth\" class=\"mx-auto max-w-md px-4 py-16 sm:px-6 lg:px-8\"><div class=\"card p-6\"><h2 class=\"mb-5 text-center text-lg font-bold text-content\">Sign in to Bookhoard</h2><form hx-post=\"/api/auth/login\" hx-target=\"#auth-result\" hx-swap=\"innerHTML\" class=\"space-y-4\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full\">Sign In</button></form></div><div id=\"auth-result\" class=\"mt-6 text-center\"></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, 3, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+33 -39
View File
@@ -6,60 +6,54 @@ templ Login(sessionExpired bool, deleted bool) {
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Login</title> <title>Login - Bookhoard</title>
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
<script type="module" src="/static/main.js"></script> <script type="module" src="/static/main.js"></script>
<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="login" x-init="initLoginTheme()"> <body class="theme-tokyo-night flex min-h-screen items-center justify-center px-4" x-data="login" x-init="initLoginTheme()">
<div class="container mx-auto px-4 py-8 max-w-md"> <div class="w-full max-w-md">
<div class="flex justify-between items-center mb-8"> <div class="mb-8 flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-content">
<h1 class="text-3xl font-bold">Login to Bookhoard</h1> @Icon("book-open", "h-7 w-7 text-brand")
<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)"> Bookhoard
<option value="tokyo-night">Tokyo Night</option> </div>
<option value="dracula">Dracula</option> <div class="mb-6 flex justify-end">
<option value="nord">Nord</option> <select id="theme-select" @change="changeTheme()" class="input w-auto py-1.5 text-xs">
<option value="solarized-dark">Solarized Dark</option> for _, t := range ThemeOptions {
<option value="monokai">Monokai</option> <option value={ t.Name }>{ t.Label }</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> </select>
</div> </div>
<div class="card p-6">
<h1 class="mb-5 text-xl font-bold text-content">Welcome back</h1>
if sessionExpired { if sessionExpired {
<div class="mb-4 p-3 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--accent);"> <div class="mb-4 flex items-start gap-2 rounded-lg border border-brand/40 bg-brand/10 p-3 text-sm text-content">
<p style="color: var(--text-primary);"> @Icon("info", "h-4 w-4 mt-0.5 shrink-0 text-brand")
Your session has expired. Please log in again to continue. <span>Your session has expired. Please log in again to continue.</span>
</p>
</div> </div>
} }
if deleted { if deleted {
<div class="mb-4 p-3 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--accent);"> <div class="mb-4 flex items-start gap-2 rounded-lg border border-success/40 bg-success/10 p-3 text-sm text-content">
<p style="color: var(--text-primary);"> @Icon("check-circle", "h-4 w-4 mt-0.5 shrink-0 text-success")
Your account has been deleted successfully. <span>Your account has been deleted successfully.</span>
</p>
</div> </div>
} }
<form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border"> <form hx-post="/api/auth/login" hx-target="#result" hx-swap="innerHTML" class="space-y-4">
<div class="mb-4"> <div>
<label class="block mb-2" style="color: var(--text-secondary)">Email or Username</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="text" name="login" class="input" required/>
</div> </div>
<div class="mb-4"> <div>
<label class="block mb-2" style="color: var(--text-secondary)">Password</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="password" name="password" class="input" required/>
</div> </div>
<button type="submit" class="btn-primary w-full py-2 rounded"> <button type="submit" class="btn btn-primary w-full">Sign In</button>
Sign In
</button>
</form> </form>
<div id="result" class="mt-4 text-center"></div> <div id="result" class="mt-4 text-center text-sm"></div>
<p class="text-center mt-4"> <div class="mt-4 border-t border-line pt-4 text-center">
<a href="/register" class="text-blue-500 hover:underline">Don't have an account? Sign Up</a> <a href="/register" class="text-sm text-content-muted transition-colors hover:text-content">Don't have an account? Sign Up</a>
</p> </div>
</div>
</div> </div>
</body> </body>
</html> </html>
+64 -4
View File
@@ -29,23 +29,83 @@ 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 flex min-h-screen items-center justify-center px-4\" x-data=\"login\" x-init=\"initLoginTheme()\"><div class=\"w-full max-w-md\"><div class=\"mb-8 flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-7 w-7 text-brand").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Bookhoard</div><div class=\"mb-6 flex justify-end\"><select id=\"theme-select\" @change=\"changeTheme()\" class=\"input w-auto py-1.5 text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, t := range ThemeOptions {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(t.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 29}
}
_, 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, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(t.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 23, Col: 41}
}
_, 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, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</select></div><div class=\"card p-6\"><h1 class=\"mb-5 text-xl font-bold text-content\">Welcome back</h1>")
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, 7, "<div class=\"mb-4 flex items-start gap-2 rounded-lg border border-brand/40 bg-brand/10 p-3 text-sm text-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("info", "h-4 w-4 mt-0.5 shrink-0 text-brand").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>Your session has expired. Please log in again to continue.</span></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, 9, "<div class=\"mb-4 flex items-start gap-2 rounded-lg border border-success/40 bg-success/10 p-3 text-sm text-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-4 w-4 mt-0.5 shrink-0 text-success").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span>Your account has been deleted successfully.</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, 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, 11, "<form hx-post=\"/api/auth/login\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"space-y-4\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Email or Username</label> <input type=\"text\" name=\"login\" class=\"input\" required></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Password</label> <input type=\"password\" name=\"password\" class=\"input\" required></div><button type=\"submit\" class=\"btn btn-primary w-full\">Sign In</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><div class=\"mt-4 border-t border-line pt-4 text-center\"><a href=\"/register\" class=\"text-sm text-content-muted transition-colors hover:text-content\">Don't have an account? Sign Up</a></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+6 -6
View File
@@ -13,18 +13,18 @@ templ Profile(user User) {
@Header(user, "/profile") @Header(user, "/profile")
<main class="max-w-4xl mx-auto px-4 py-8"> <main class="max-w-4xl mx-auto px-4 py-8">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">Profile Settings</h1> <h1 class="text-2xl font-bold tracking-tight mb-2 text-content">Profile Settings</h1>
<p style="color: var(--text-secondary)">Manage your account information and preferences</p> <p class="text-content-muted">Manage your account information and preferences</p>
</div> </div>
<div class="space-y-8"> <div class="space-y-8">
<!-- Account Information & Password - uses reusable ProfileForm --> <!-- Account Information & Password - uses reusable ProfileForm -->
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="card p-6">
@ProfileForm(user, "/api/auth/profile", true, false, false) @ProfileForm(user, "/api/auth/profile", true, false, false)
</div> </div>
<!-- Danger Zone --> <!-- Danger Zone -->
<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)">Danger Zone</h3> <h3 class="text-xl font-semibold mb-4 text-content">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> <p class="text-content-muted 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;"> <button @click="confirmDeleteAccount()" class="px-4 py-2 rounded text-sm" style="background-color: #dc2626; color: white;">
Remove My Account Remove My Account
</button> </button>
+32 -48
View File
@@ -10,56 +10,51 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
> >
<!-- Account Information --> <!-- Account Information -->
<div> <div>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Account Information</h3> <h3 class="text-lg font-semibold mb-4 text-content">Account Information</h3>
<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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 +66,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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 +99,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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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>
@@ -120,37 +113,34 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
</div> </div>
<!-- Change Password Section --> <!-- Change Password Section -->
<div> <div>
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Change Password</h3> <h3 class="text-lg font-semibold mb-4 text-content">Change Password</h3>
if requireCurrentPassword { if requireCurrentPassword {
<!-- Self-edit: require current password --> <!-- 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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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,8 +151,7 @@ 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;"
> >
Update Password Update Password
</button> </button>
@@ -170,27 +159,25 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
</div> </div>
} else { } else {
<!-- Admin edit: no current password required --> <!-- 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 text-content-muted">
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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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,8 +188,7 @@ 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;"
> >
Reset Password Reset Password
</button> </button>
@@ -211,21 +197,19 @@ templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, show
} }
</div> </div>
<!-- Actions --> <!-- 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 border-line">
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;"
> >
Save Changes Save Changes
</button> </button>
+18 -18
View File
@@ -42,7 +42,7 @@ 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-6\"><!-- Account Information --><div><h3 class=\"text-lg font-semibold mb-4 text-content\">Account Information</h3><div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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
} }
@@ -55,46 +55,46 @@ 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, 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, 3, "\" class=\"input\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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: 29, 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, 4, "\" class=\"input\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.FirstName) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.FirstName)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 40, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 38, Col: 28}
} }
_, 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, 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, 5, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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: 48, 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, 6, "\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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
} }
@@ -164,7 +164,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
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, 20, ">Material Dark</option></select></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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
} }
@@ -409,7 +409,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
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, 69, "<div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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
} }
@@ -434,45 +434,45 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
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, 74, "</div></div><!-- Change Password Section --><div><h3 class=\"text-lg font-semibold mb-4 text-content\">Change Password</h3>")
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, 75, "<!-- Self-edit: require current password --> <div class=\"space-y-4 max-w-md\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Current Password</label> <input type=\"password\" name=\"current_password\" class=\"input\" required></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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\">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, 76, "<!-- Admin edit: no current password required --> <p class=\"text-sm italic mb-4 text-content-muted\">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=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">New Password</label> <input type=\"password\" name=\"new_password\" class=\"input\" minlength=\"6\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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: 186, 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, 77, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"#password-result\" hx-swap=\"innerHTML\" hx-include=\"closest form\" class=\"btn btn-primary\">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, 78, "</div><!-- Actions --><div class=\"flex justify-end gap-3 pt-6 border-t border-line\">")
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, 79, "<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, 80, "<button type=\"submit\" class=\"btn btn-primary\">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
} }
+7 -8
View File
@@ -1,22 +1,21 @@
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" 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 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<!-- Modal Header --> <!-- Modal Header -->
<div class="flex justify-between items-center p-6 border-b" style="border-color: var(--border);"> <div class="flex justify-between items-center p-6 border-b border-line">
<div> <div>
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Edit User Profile</h2> <h2 class="text-2xl font-bold tracking-tight text-content">Edit User Profile</h2>
<p class="text-sm mt-1" style="color: var(--text-secondary);"> <p class="text-sm mt-1 text-content-muted">
{ user.Username } ({ user.Email }) { user.Username } ({ user.Email })
</p> </p>
</div> </div>
<button <button
@click="closeProfileModal()" @click="closeProfileModal()"
class="text-2xl" class="btn btn-ghost"
style="color: var(--text-secondary);"
> >
&times; @Icon("close", "h-5 w-5")
</button> </button>
</div> </div>
+11 -3
View File
@@ -29,7 +29,7 @@ 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\" x-data=\"profileModal\" x-init=\"setupProfileModal()\" style=\"background-color: var(--surface-overlay);\"><div class=\"card max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto\"><!-- Modal Header --><div class=\"flex justify-between items-center p-6 border-b border-line\"><div><h2 class=\"text-2xl font-bold tracking-tight text-content\">Edit User Profile</h2><p class=\"text-sm mt-1 text-content-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -55,7 +55,15 @@ 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, 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, 3, ")</p></div><button @click=\"closeProfileModal()\" class=\"btn btn-ghost\">")
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, 4, "</button></div><!-- Modal Body - uses reusable form --><div class=\"p-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -63,7 +71,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, 5, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+2 -2
View File
@@ -37,7 +37,7 @@ 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=\"max-w-4xl mx-auto px-4 py-8\"><div class=\"mb-8\"><h1 class=\"text-2xl font-bold tracking-tight mb-2 text-content\">Profile Settings</h1><p class=\"text-content-muted\">Manage your account information and preferences</p></div><div class=\"space-y-8\"><!-- Account Information & Password - uses reusable ProfileForm --><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 +45,7 @@ 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, 3, "</div><!-- Danger Zone --><div class=\"card p-6\"><h3 class=\"text-xl font-semibold mb-4 text-content\">Danger Zone</h3><p class=\"text-content-muted 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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+26 -24
View File
@@ -1,7 +1,9 @@
package templates package templates
import "bookhoard/internal/handlers" import (
import "fmt" "bookhoard/internal/handlers"
"fmt"
)
templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage string) { templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage string) {
<!DOCTYPE html> <!DOCTYPE html>
@@ -17,19 +19,19 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
@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">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Reading Progress</h1> <h1 class="text-2xl font-bold tracking-tight text-content">Reading Progress</h1>
<p style="color: var(--text-secondary)">Track your reading progress across all devices</p> <p class="text-content-muted">Track your reading progress across all devices</p>
</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="text-center py-16 text-content-muted">
<div class="text-6xl mb-4">📖</div> <div class="mb-4 flex justify-center">@Icon("book-open", "h-12 w-12")</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 text-content">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
@@ -44,27 +46,27 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
<div> <div>
<a <a
href={ "/media/" + item.MediaItemID.String() } href={ "/media/" + item.MediaItemID.String() }
style="color: var(--text-primary); text-decoration: none;" class="text-content no-underline"
> >
<h3 class="font-semibold text-lg hover:opacity-80">{ item.Title }</h3> <h3 class="font-semibold text-lg hover:opacity-80">{ 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 text-content-muted">by { item.Author }</p>
} }
</div> </div>
<div class="text-right"> <div class="text-right">
<span class="text-2xl font-bold" style="color: var(--accent)">{ item.ProgressPercentage }%</span> <span class="text-2xl font-bold text-brand">{ item.ProgressPercentage }%</span>
</div> </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 bg-surface rounded-full h-3">
<div class="h-3 rounded-full transition-all" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);" }></div> <div class="h-3 rounded-full transition-all bg-brand" style={ "width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%" }></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-4 text-sm">
<div> <div>
<p style="color: var(--text-secondary)">Current Position</p> <p class="text-content-muted">Current Position</p>
<p style="color: var(--text-primary)"> <p class="text-content">
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) }
@@ -77,29 +79,29 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
</p> </p>
</div> </div>
<div> <div>
<p style="color: var(--text-secondary)">Last Updated</p> <p class="text-content-muted">Last Updated</p>
<p style="color: var(--text-primary)">{ item.LastUpdated }</p> <p class="text-content">{ item.LastUpdated }</p>
</div> </div>
<div> <div>
<p style="color: var(--text-secondary)">Synced From</p> <p class="text-content-muted">Synced From</p>
<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-content">{ 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 border-line">
<div class="flex items-center gap-2 text-xs" style="color: var(--text-secondary);"> <div class="flex items-center gap-2 text-xs text-content-muted">
<span>🔄</span> @Icon("sync", "h-4 w-4")
<span>Last sync from <strong>{ item.DeviceName }</strong></span> <span>Last sync from <strong>{ item.DeviceName }</strong></span>
<span>({ item.DeviceType })</span> <span>({ item.DeviceType })</span>
</div> </div>
</div> </div>
} }
if item.EpubCFI != "" { if item.EpubCFI != "" {
<div class="mt-2 text-xs" style="color: var(--text-secondary);"> <div class="mt-2 text-xs text-content-muted">
<span>📍</span> @Icon("bookmark", "h-4 w-4")
<span>CFI: { item.EpubCFI }</span> <span>CFI: { item.EpubCFI }</span>
</div> </div>
} }
+65 -39
View File
@@ -8,8 +8,10 @@ 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"
import "bookhoard/internal/handlers" import (
import "fmt" "bookhoard/internal/handlers"
"fmt"
)
func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage string) templ.Component { func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage 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) {
@@ -40,93 +42,101 @@ 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\"><h1 class=\"text-2xl font-bold tracking-tight text-content\">Reading Progress</h1><p class=\"text-content-muted\">Track your reading progress across all devices</p></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, 3, "<div class=\"text-center py-16 text-content-muted\"><div class=\"mb-4 flex justify-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-12 w-12").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><h3 class=\"text-xl font-semibold mb-2 text-content\">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, 5, "<div class=\"card p-6\"><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=\"")
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("/media/" + item.MediaItemID.String()) templ_7745c5c3_Var2, 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: 46, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 48, Col: 56}
} }
_, 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, "\" 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, 6, "\" class=\"text-content no-underline\"><h3 class=\"font-semibold text-lg hover:opacity-80\">")
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(item.Title) templ_7745c5c3_Var3, 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/progress.templ`, Line: 49, Col: 75} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 51, Col: 75}
} }
_, 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, 7, "</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, 8, "<p class=\"text-sm text-content-muted\">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_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author) templ_7745c5c3_Var4, 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: 54, Col: 66}
} }
_, 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, 8, "</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</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, 10, "</div><div class=\"text-right\"><span class=\"text-2xl font-bold text-brand\">")
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(item.ProgressPercentage) templ_7745c5c3_Var5, 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: 56, Col: 98} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 58, Col: 80}
} }
_, 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, 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=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "%</span></div></div><div class=\"mb-4\"><div class=\"w-full bg-surface rounded-full h-3\"><div class=\"h-3 rounded-full transition-all bg-brand\" 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 = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", 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: 135}
} }
_, 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, 12, "\"></div></div></div><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 text-sm\"><div><p class=\"text-content-muted\">Current Position</p><p class=\"text-content\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -135,7 +145,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages)) templ_7745c5c3_Var7, 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: 70, Col: 89} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 72, Col: 89}
} }
_, 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 {
@@ -145,7 +155,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
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("%.1f%%", item.ProgressPercentage))
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: 74, Col: 61}
} }
_, 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 {
@@ -156,113 +166,129 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages)) templ_7745c5c3_Var9, 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: 77, Col: 71}
} }
_, 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, "</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, 13, "</p></div><div><p class=\"text-content-muted\">Last Updated</p><p class=\"text-content\">")
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.LastUpdated) templ_7745c5c3_Var10, 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: 81, Col: 67} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 83, Col: 53}
} }
_, 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, 13, "</p></div><div><p style=\"color: var(--text-secondary)\">Synced From</p><div class=\"flex items-center gap-2\"><span class=\"text-xl\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div><div><p class=\"text-content-muted\">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.DeviceIcon)
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: 88, Col: 51}
} }
_, 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, 15, "</span> <span class=\"text-content\">")
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.DeviceName)
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: 89, Col: 56}
} }
_, 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, 16, "</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, 17, "<div class=\"mt-3 pt-3 border-t border-line\"><div class=\"flex items-center gap-2 text-xs text-content-muted\">")
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, 18, "<span>Last sync from <strong>")
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.DeviceName) templ_7745c5c3_Var13, 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: 95, Col: 58} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 97, Col: 58}
} }
_, 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, "</strong></span> <span>(") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</strong></span> <span>(")
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.DeviceType)
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: 98, Col: 36}
} }
_, 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, 20, ")</span></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if item.EpubCFI != "" { 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: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"mt-2 text-xs text-content-muted\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("bookmark", "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, 22, "<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.EpubCFI)
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: 105, Col: 36}
} }
_, 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, 23, "</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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</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, 25, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -270,7 +296,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, 26, "</body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+49 -47
View File
@@ -18,49 +18,48 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
<div class="mb-8"> <div class="mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Queue</h1> <h1 class="text-2xl font-bold tracking-tight text-content">Sync Queue</h1>
<p style="color: var(--text-secondary)">Monitor and manage offline sync queue</p> <p class="text-content-muted">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">
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 Clear Failed
</button> </button>
<button @click="clearAllItems()" class="btn-danger px-4 py-2 rounded-lg"> <button @click="clearAllItems()" class="btn btn-danger">
Clear All Clear All
</button> </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="card p-5 text-center">
<div class="text-3xl font-bold" style="color: #f59e0b;">{ stats.PendingCount }</div> <div class="text-2xl font-bold text-warning">{ stats.PendingCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Pending</div> <div class="text-xs font-semibold uppercase tracking-wide text-content-muted">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="card p-5 text-center">
<div class="text-3xl font-bold" style="color: #3b82f6;">{ stats.ProcessingCount }</div> <div class="text-2xl font-bold text-info">{ stats.ProcessingCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Processing</div> <div class="text-xs font-semibold uppercase tracking-wide text-content-muted">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="card p-5 text-center">
<div class="text-3xl font-bold" style="color: #10b981;">{ stats.CompletedCount }</div> <div class="text-2xl font-bold text-success">{ stats.CompletedCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Completed</div> <div class="text-xs font-semibold uppercase tracking-wide text-content-muted">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="card p-5 text-center">
<div class="text-3xl font-bold" style="color: #ef4444;">{ stats.FailedCount }</div> <div class="text-2xl font-bold text-danger">{ stats.FailedCount }</div>
<div class="text-sm" style="color: var(--text-secondary);">Failed</div> <div class="text-xs font-semibold uppercase tracking-wide text-content-muted">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-center">
<div> <div>
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Status Filter</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 w-auto"
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 +69,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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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 w-auto"
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,78 +83,82 @@ 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="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Device</label>
<select <select
id="device-filter" id="device-filter"
onchange="filterQueue()" onchange="filterQueue()"
class="px-4 py-2 border rounded-lg" class="input w-auto"
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>
</div> </div>
<div id="loading" class="hidden text-center py-16" style="color: var(--text-secondary)"> <div id="loading" class="hidden text-center py-16 text-content-muted">
<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 text-center py-16 text-content-muted">
<div class="text-6xl mb-4">📭</div> <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3> @Icon("sync", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">Queue Empty</h3>
<p>No items in the sync queue</p> <p>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="text-center py-16 text-content-muted">
<div class="text-6xl mb-4">📭</div> <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">Queue Empty</h3> @Icon("sync", "h-7 w-7")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">Queue Empty</h3>
<p>No items in the sync queue</p> <p>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">
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2">
<div class="flex items-center space-x-3"> <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 }">{ item.Status }</span> <span class="inline-block px-2 py-0.5 rounded text-[11px] font-semibold uppercase status-{ item.Status }">{ item.Status }</span>
<span class="text-sm" style="color: var(--text-secondary)">{ item.SyncType }</span> <span class="text-sm text-content-muted">{ 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="inline-block px-2 py-0.5 rounded text-[10px] font-semibold 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 text-content-muted">
Device ID: { item.DeviceID } Device ID: { item.DeviceID }
if item.MediaTitle != nil { if item.MediaTitle != nil {
| Book: { *item.MediaTitle } | Book: { *item.MediaTitle }
} }
</div> </div>
<div class="flex justify-between items-center text-sm" style="color: var(--text-secondary)"> <div class="flex justify-between items-center text-sm text-content-muted">
<span>Attempts: { item.Attempts }/{ item.MaxAttempts }</span> <span>Attempts: { item.Attempts }/{ item.MaxAttempts }</span>
<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 text-danger">
Error: { *item.ErrorMessage } Error: { *item.ErrorMessage }
</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" 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 mx-4 my-8">
<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-2xl font-bold tracking-tight text-content">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">Retry</button>
</div> </div>
</div> </div>
</div> </div>
+65 -33
View File
@@ -39,208 +39,240 @@ 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=\"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-2xl font-bold tracking-tight text-content\">Sync Queue</h1><p class=\"text-content-muted\">Monitor and manage offline sync queue</p></div><div class=\"flex space-x-3\"><button @click=\"processPendingItems()\" class=\"btn btn-primary\">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=\"card p-5 text-center\"><div class=\"text-2xl font-bold text-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: 38, Col: 72}
} }
_, 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, 3, "</div><div class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Pending</div></div><div class=\"card p-5 text-center\"><div class=\"text-2xl font-bold text-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: 42, Col: 72}
} }
_, 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, 4, "</div><div class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Processing</div></div><div class=\"card p-5 text-center\"><div class=\"text-2xl font-bold text-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: 46, Col: 74}
} }
_, 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, 5, "</div><div class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Completed</div></div><div class=\"card p-5 text-center\"><div class=\"text-2xl font-bold text-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: 50, Col: 70}
} }
_, 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, 6, "</div><div class=\"text-xs font-semibold uppercase tracking-wide text-content-muted\">Failed</div></div></div></div><div class=\"card p-4 mb-6\"><div class=\"flex flex-wrap gap-4 items-center\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Status Filter</label> <select id=\"status-filter\" onchange=\"filterQueue()\" class=\"input w-auto\"><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=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Sync Type</label> <select id=\"type-filter\" onchange=\"filterQueue()\" class=\"input w-auto\"><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=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Device</label> <select id=\"device-filter\" onchange=\"filterQueue()\" class=\"input w-auto\"><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, 7, "Refresh</button></div></div><div id=\"loading\" class=\"hidden text-center py-16 text-content-muted\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading queue...</p></div><div id=\"empty-state\" class=\"hidden text-center py-16 text-content-muted\"><div class=\"mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "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, 8, "</div><h3 class=\"text-xl font-semibold mb-2 text-content\">Queue Empty</h3><p>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, 9, "<div class=\"text-center py-16 text-content-muted\"><div class=\"mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand/15 text-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "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, "</div><h3 class=\"text-xl font-semibold mb-2 text-content\">Queue Empty</h3><p>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, 11, "<div class=\"card p-4\"><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 }\">")
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: 127, Col: 128}
} }
_, 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, 12, "</span> <span class=\"text-sm text-content-muted\">")
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: 128, Col: 65}
} }
_, 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, 13, "</span></div><span class=\"inline-block px-2 py-0.5 rounded text-[10px] font-semibold 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: 130, Col: 124}
} }
_, 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, 14, "</span></div><div class=\"text-sm mb-2 text-content-muted\">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: 133, 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, 15, " ")
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, 16, "| 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: 135, 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, 17, "</div><div class=\"flex justify-between items-center text-sm text-content-muted\"><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: 139, 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, 18, "/")
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: 139, 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, 19, "</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: 140, 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, 20, "</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, 21, "<div class=\"mt-2 text-sm text-danger\">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: 144, Col: 36}
} }
_, 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, 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><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, 24, "</div><div id=\"queue-item-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-2xl mx-4 my-8\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-2xl font-bold tracking-tight text-content\">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, 25, "</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\">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
} }
+45 -49
View File
@@ -6,70 +6,66 @@ templ Register() {
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Register</title> <title>Register - Bookhoard</title>
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
<script type="module" src="/static/main.js"></script> <script type="module" src="/static/main.js"></script>
<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="register" x-init="initRegisterTheme()"> <body class="theme-tokyo-night flex min-h-screen items-center justify-center px-4 py-8" x-data="register" x-init="initRegisterTheme()">
<div class="container mx-auto px-4 py-8 max-w-md"> <div class="w-full max-w-md">
<h1 class="text-3xl font-bold text-center mb-8">Register for Bookhoard</h1> <div class="mb-6 flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-content">
<form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="card p-6 rounded-lg shadow-md border"> @Icon("book-open", "h-7 w-7 text-brand")
<div class="mb-4"> Bookhoard
<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>
<div class="mb-4"> <div class="card p-6">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label> <h1 class="mb-5 text-xl font-bold text-content">Create your account</h1>
<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/> <form hx-post="/api/auth/register" hx-target="#result" hx-swap="innerHTML" class="space-y-4">
<div>
<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Email</label>
<input type="email" id="email" name="email" class="input" required/>
</div> </div>
<div class="mb-4"> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Username</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)"/> <input type="text" id="username" name="username" class="input" required/>
</div> </div>
<div class="mb-4"> <div class="grid grid-cols-2 gap-3">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label> <div>
<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)"/> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">First Name</label>
<input type="text" name="first_name" placeholder="Optional" class="input"/>
</div> </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)"> <div>
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">Last Name</label>
<ul class="space-y-1" style="color: var(--text-secondary)"> <input type="text" name="last_name" placeholder="Optional" class="input"/>
<li id="req-length" class="flex items-center gap-2"> </div>
<span class="requirement-icon"></span> At least 8 characters </div>
</li> <div id="password-requirements" class="rounded-xl border border-line bg-surface p-4 text-sm">
<li id="req-upper" class="flex items-center gap-2"> <p class="mb-2 font-semibold text-content">Password Requirements</p>
<span class="requirement-icon"></span> One uppercase letter <ul class="space-y-1.5 text-content-muted">
</li> <li id="req-length" class="flex items-center gap-2"><span class="requirement-icon"></span> At least 8 characters</li>
<li id="req-lower" class="flex items-center gap-2"> <li id="req-upper" class="flex items-center gap-2"><span class="requirement-icon"></span> One uppercase letter</li>
<span class="requirement-icon"></span> One lowercase letter <li id="req-lower" class="flex items-center gap-2"><span class="requirement-icon"></span> One lowercase letter</li>
</li> <li id="req-number" class="flex items-center gap-2"><span class="requirement-icon"></span> One number</li>
<li id="req-number" class="flex items-center gap-2"> <li id="req-special" class="flex items-center gap-2"><span class="requirement-icon"></span> One special character</li>
<span class="requirement-icon"></span> One number <li id="req-match" class="flex items-center gap-2"><span class="requirement-icon"></span> Passwords match</li>
</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> </ul>
</div> </div>
<div class="mb-4"> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="password" id="password" name="password" class="input" required/>
</div> </div>
<div class="mb-4"> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label> <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted">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/> <input type="password" id="confirm-password" name="confirm_password" class="input" required/>
</div> </div>
<button type="submit" id="register-btn" class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed" disabled> <button type="submit" id="register-btn" class="btn btn-primary w-full opacity-50 cursor-not-allowed" disabled>
Register Register
</button> </button>
</form> </form>
<div id="result" class="mt-4 text-center"></div> <div id="result" class="mt-4 text-center text-sm"></div>
<p class="text-center mt-4"> <div class="mt-4 border-t border-line pt-4 text-center">
<a href="/login" class="text-blue-500 hover:underline">Already have an account? Login</a> <a href="/login" class="text-sm text-content-muted transition-colors hover:text-content">Already have an account? Login</a>
</p> </div>
</div>
</div> </div>
</body> </body>
</html> </html>
+9 -1
View File
@@ -29,7 +29,15 @@ 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 flex min-h-screen items-center justify-center px-4 py-8\" x-data=\"register\" x-init=\"initRegisterTheme()\"><div class=\"w-full max-w-md\"><div class=\"mb-6 flex items-center justify-center gap-2 text-2xl font-bold tracking-tight text-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("book-open", "h-7 w-7 text-brand").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Bookhoard</div><div class=\"card p-6\"><h1 class=\"mb-5 text-xl font-bold text-content\">Create your account</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"space-y-4\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"input\" required></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"input\" required></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"input\"></div></div><div id=\"password-requirements\" class=\"rounded-xl border border-line bg-surface p-4 text-sm\"><p class=\"mb-2 font-semibold text-content\">Password Requirements</p><ul class=\"space-y-1.5 text-content-muted\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"input\" required></div><div><label class=\"mb-1.5 block text-xs font-semibold uppercase tracking-wide text-content-muted\">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 opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><div class=\"mt-4 border-t border-line pt-4 text-center\"><a href=\"/login\" class=\"text-sm text-content-muted transition-colors hover:text-content\">Already have an account? Login</a></div></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -1,19 +1,21 @@
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" 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">
<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> <h2 class="text-xl font-bold text-content">Restore System Collection</h2>
<button type="button" @click="closeCollectionModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button> <button type="button" @click="closeCollectionModal()" class="icon-btn">
@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 text-content-muted">Select a system collection to restore:</p>
<form <form
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 +24,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,15 @@ 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\" 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 text-content\">Restore System Collection</h2><button type=\"button\" @click=\"closeCollectionModal()\" class=\"icon-btn\">")
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, 2, "</button></div><div class=\"mb-4\"><p class=\"mb-4 text-content-muted\">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
} }
+92 -98
View File
@@ -14,8 +14,8 @@ templ Setup() {
<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="container mx-auto 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> <h1 class="text-2xl font-bold tracking-tight text-content mb-2">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 text-content-muted">Let's get your library set up.</p>
</div> </div>
<!-- Step indicator --> <!-- Step indicator -->
@@ -39,77 +39,72 @@ templ Setup() {
</div> </div>
<!-- Error display --> <!-- Error display -->
<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"> <div x-show="errorMsg" x-cloak class="mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm">
<span x-text="errorMsg"></span> <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" x-cloak 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 tracking-tight text-content mb-2">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 text-content-muted">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 p-4 rounded-lg border border-line bg-surface">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Server URL (Base URL)</label>
<input <input
type="url" type="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 w-full 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 text-content-muted">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 w-full"
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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 w-full"
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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">First Name</label>
<input <input
type="text" type="text"
x-model="admin.firstName" x-model="admin.firstName"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input w-full"
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 text-content-muted mb-1">Last Name</label>
<input <input
type="text" type="text"
x-model="admin.lastName" x-model="admin.lastName"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input w-full"
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 p-4 rounded-lg border border-line bg-surface text-sm">
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p> <p class="font-semibold mb-2 text-content">Password Requirements:</p>
<ul class="space-y-1" style="color: var(--text-secondary)"> <ul class="space-y-1 text-content-muted">
<li id="req-length" class="flex items-center gap-2"> <li id="req-length" class="flex items-center gap-2">
<span class="requirement-icon"></span> At least 8 characters <span class="requirement-icon"></span> At least 8 characters
</li> </li>
@@ -131,31 +126,29 @@ 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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 w-full"
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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">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 w-full"
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 opacity-50 cursor-not-allowed"
:disabled="loading" :disabled="loading"
x-text="loading ? 'Creating...' : 'Create Admin Account'" x-text="loading ? 'Creating...' : 'Create Admin Account'"
></button> ></button>
@@ -163,49 +156,47 @@ templ Setup() {
</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" x-cloak 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 tracking-tight text-content mb-2">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 text-content-muted">Add one or more media libraries. You can always add more later.</p>
<!-- Created libraries list --> <!-- Created libraries list -->
<div x-show="libraries.length > 0" class="mb-6 space-y-2"> <div x-show="libraries.length > 0" x-cloak 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="p-3 rounded-lg border border-line bg-surface flex items-center justify-between">
<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="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<div> <div>
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p> <p class="font-medium text-content" 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 text-content-muted" x-text="lib.type"></p>
</div> </div>
</div> </div>
<button <button
type="button" type="button"
@click="removeLibrary(idx)" @click="removeLibrary(idx)"
class="text-xs text-red-500 hover:underline" class="text-xs text-danger hover:underline"
>Remove</button> >Remove</button>
</div> </div>
</template> </template>
</div> </div>
<!-- Add library form --> <!-- Add library form -->
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)"> <form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border border-line bg-surface">
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Name</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Name</label>
<input <input
type="text" type="text"
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 w-full"
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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Type</label>
<select <select
x-model="newLibrary.type" x-model="newLibrary.type"
class="w-full px-3 py-2 border rounded" class="input w-full"
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>
@@ -213,54 +204,53 @@ templ Setup() {
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label> <label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Description (optional)</label>
<input <input
type="text" type="text"
x-model="newLibrary.description" x-model="newLibrary.description"
placeholder="Optional" placeholder="Optional"
class="w-full px-3 py-2 border rounded" class="input w-full"
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">Back</button>
<button type="button" @click="currentStep = 3" class="btn-primary px-6 py-2 rounded"> <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>
</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" x-cloak 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 tracking-tight text-content mb-2">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 text-content-muted">Add the filesystem folders where your media files are stored.</p>
<div x-show="libraries.length === 0" class="text-center py-8" style="color: var(--text-secondary)"> <div x-show="libraries.length === 0" x-cloak class="text-center py-8 text-content-muted">
<p>No libraries created. You can add folders later from the admin panel.</p> <p>No libraries created. You can add folders later from the admin panel.</p>
</div> </div>
<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="p-4 rounded-lg border border-line bg-surface">
<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="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4> <h4 class="font-semibold text-content" x-text="lib.name"></h4>
</div> </div>
<!-- Existing folders for this library --> <!-- 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 text-sm border border-line bg-surface-raised">
<span style="color: var(--text-primary)" x-text="folder"></span> <span class="text-content" x-text="folder"></span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-red-500 hover:underline">Remove</button> <button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-danger hover:underline">Remove</button>
</div> </div>
</template> </template>
</div> </div>
@@ -271,18 +261,17 @@ templ Setup() {
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 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 text-xs"
>Add</button> >Add</button>
</div> </div>
</div> </div>
@@ -290,37 +279,37 @@ 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">Back</button>
<button type="button" @click="currentStep = 4" class="btn-primary px-6 py-2 rounded">Next: Scan</button> <button type="button" @click="currentStep = 4" class="btn btn-primary">Next: Scan</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" x-cloak 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 tracking-tight text-content mb-2">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 text-content-muted">Review your configuration and start the initial scan.</p>
<!-- Summary --> <!-- 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="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL</p> <p class="text-sm font-medium mb-1 text-content-muted">Server URL</p>
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p> <p class="text-content" 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="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Administrator</p> <p class="text-sm font-medium mb-1 text-content-muted">Administrator</p>
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p> <p class="text-content" 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="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-2" style="color: var(--text-secondary)"> <p class="text-sm font-medium mb-2 text-content-muted">
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>
<span style="color: var(--text-primary)" x-text="lib.name"></span> <span class="text-content" 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="ml-2 text-xs px-2 py-0.5 rounded bg-brand text-surface" x-text="lib.type"></span>
</div> </div>
<div class="text-right" style="color: var(--text-secondary)"> <div class="text-right text-content-muted">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span> <span x-text="(lib.folders || []).length + ' folder(s)'"></span>
</div> </div>
</div> </div>
@@ -329,16 +318,16 @@ templ Setup() {
</div> </div>
<!-- Scan progress --> <!-- Scan progress -->
<div x-show="scanStarted" class="mb-6 space-y-3"> <div x-show="scanStarted" x-cloak class="mb-6 space-y-3">
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3> <h3 class="font-semibold text-content">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="p-3 rounded border border-line bg-surface">
<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 text-content" x-text="job.libraryName"></span>
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span> <span class="text-sm text-content-muted" x-text="job.statusText"></span>
</div> </div>
<div class="w-full rounded-full h-2" style="background-color: var(--bg-secondary)"> <div class="w-full rounded-full h-2 bg-surface-raised">
<div <div
class="h-2 rounded-full transition-all duration-500" class="h-2 rounded-full transition-all duration-500"
:style="'width: ' + job.progress + '%; background-color: var(--accent);'" :style="'width: ' + job.progress + '%; background-color: var(--accent);'"
@@ -347,36 +336,39 @@ 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" x-cloak class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
Initial scan complete! Your libraries are ready to use. 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">Back</button>
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
x-show="!scanStarted" x-show="!scanStarted"
x-cloak
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> >Start Scan</button>
<button <button
x-show="scanStarted && !scanComplete" x-show="scanStarted && !scanComplete"
x-cloak
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>
<button <button
x-show="scanComplete" x-show="scanComplete"
x-cloak
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>
@@ -386,11 +378,13 @@ templ Setup() {
</div> </div>
<!-- Folder Browser Modal (reused from admin library) --> <!-- Folder Browser Modal (reused from admin library) -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7)"> <div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: var(--surface-overlay);">
<div class="card 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">
<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 text-content">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">
@Icon("close", "h-5 w-5")
</button>
</div> </div>
<div id="folder-browser-content"> <div id="folder-browser-content">
<!-- Directory listings rendered by Alpine --> <!-- Directory listings rendered by Alpine -->
File diff suppressed because one or more lines are too long
+60 -54
View File
@@ -14,84 +14,89 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
@Header(user, "/unlinked") @Header(user, "/unlinked")
<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">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Unlinked Books</h1> <h1 class="text-2xl font-bold tracking-tight text-content">Unlinked Books</h1>
<p style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p> <p class="text-content-muted">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="flex justify-between items-center mb-6 p-4 rounded-lg border border-line bg-surface-raised">
<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-content">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 text-content-muted">0 selected</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<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("info", "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("arrow-right", "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="text-center py-16 text-content-muted">
<div class="text-6xl mb-4"></div> <div class="mb-4 flex justify-center">
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">All Books Linked!</h3> @Icon("check-circle", "h-12 w-12 text-success")
</div>
<h3 class="text-xl font-semibold mb-2 text-content">All Books Linked!</h3>
<p>There are no unlinked books that need manual resolution.</p> <p>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">
<div class="flex items-center gap-3 mb-4"> <div class="flex items-center gap-3 mb-4">
<div class="text-4xl"> <div>
if book.DeviceType == "koreader" { if book.DeviceType == "koreader" {
📖 @Icon("book-open", "h-8 w-8")
} else if book.DeviceType == "kobo" { } else if book.DeviceType == "kobo" {
📚 @Icon("book", "h-8 w-8")
} else { } else {
📱 @Icon("device", "h-8 w-8")
} }
</div> </div>
<div> <div>
<h3 class="font-semibold text-lg" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3> <h3 class="font-semibold text-lg text-content">{ book.TitleFromDevice }</h3>
<p class="text-sm" style="color: var(--text-secondary)"> <p class="text-sm text-content-muted">
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>
<span style="color: var(--text-secondary)">File Path:</span> <span class="text-content-muted">File Path:</span>
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;"> <code class="text-content bg-surface px-1.5 py-0.5 rounded">
{ book.FilePath } { book.FilePath }
</code> </code>
</div> </div>
if book.SHA256 != "" { if book.SHA256 != "" {
<div> <div>
<span style="color: var(--text-secondary)">SHA-256:</span> <span class="text-content-muted">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="text-content bg-surface px-1.5 py-0.5 rounded text-[11px]">
{ book.SHA256 } { book.SHA256 }
</code> </code>
</div> </div>
} }
<div> <div>
<span style="color: var(--text-secondary)">Last Synced:</span> <span class="text-content-muted">Last Synced:</span>
<span style="color: var(--text-primary)">{ book.LastSyncTime }</span> <span class="text-content">{ book.LastSyncTime }</span>
</div> </div>
if book.ConfidenceScore > 0 { if book.ConfidenceScore > 0 {
<div> <div>
<span style="color: var(--text-secondary)">Auto-Link Confidence:</span> <span class="text-content-muted">Auto-Link Confidence:</span>
<span style="color: var(--text-primary)"> <span class="text-content">
{ book.ConfidenceScore }% confidence { book.ConfidenceScore }% confidence
</span> </span>
</div> </div>
@@ -100,22 +105,24 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
<div class="mb-4"> <div class="mb-4">
<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("arrow-right", "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 border-line pt-4">
<h4 class="font-semibold mb-3" style="color: var(--text-primary)">Potential Matches</h4> <h4 class="font-semibold mb-3 text-content">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 class="text-content-muted">Click "Find Potential Matches" to search</p>
</div> </div>
</div> </div>
</div> </div>
@@ -126,48 +133,48 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
</div> </div>
</div> </div>
<!-- Manual Link Modal --> <!-- 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 id="manual-link-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" 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">
<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 text-content">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">
@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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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 w-full text-content-muted"
style="background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);"
/> />
</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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">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"
> >
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-content-muted">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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2">Confidence Score</label>
<input <input
type="number" type="number"
id="link-confidence" id="link-confidence"
@@ -175,16 +182,15 @@ 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 w-full"
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 text-content-muted">1.0 = manual override (highest confidence)</p>
</div> </div>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-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">
Link Book Link Book
</button> </button>
</div> </div>
+84 -28
View File
@@ -37,164 +37,220 @@ 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, "<div 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 text-content\">Unlinked Books</h1><p class=\"text-content-muted\">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, 3, "<div class=\"flex justify-between items-center mb-6 p-4 rounded-lg border border-line bg-surface-raised\"><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-content\">Select All</label> <span id=\"selected-count\" class=\"text-sm text-content-muted\">0 selected</span></div><div class=\"flex gap-2\"><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, 4, "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("info", "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, "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("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, "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, 7, "<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, 8, "<div class=\"text-center py-16 text-content-muted\"><div class=\"mb-4 flex justify-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("check-circle", "h-12 w-12 text-success").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-xl font-semibold mb-2 text-content\">All Books Linked!</h3><p>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, 10, "<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\"><div class=\"flex items-center gap-3 mb-4\"><div>")
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-8 w-8").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("book", "h-8 w-8").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-8 w-8").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, 11, "</div><div><h3 class=\"font-semibold text-lg text-content\">")
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: 71, Col: 80}
} }
_, 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, 12, "</h3><p class=\"text-sm text-content-muted\">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: 73, 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, 13, " (")
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: 73, 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, 14, ")</p></div></div><div class=\"space-y-2 text-sm mb-4\"><div><span class=\"text-content-muted\">File Path:</span> <code class=\"text-content bg-surface px-1.5 py-0.5 rounded\">")
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: 81, 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, 15, "</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, 16, "<div><span class=\"text-content-muted\">SHA-256:</span> <code class=\"text-content bg-surface px-1.5 py-0.5 rounded text-[11px]\">")
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.JoinStringErrs(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: 88, Col: 26}
} }
_, 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, 16, "</code></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</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, 18, "<div><span class=\"text-content-muted\">Last Synced:</span> <span class=\"text-content\">")
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(book.LastSyncTime) templ_7745c5c3_Var7, 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: 94, Col: 57}
} }
_, 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, "</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</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, 20, "<div><span class=\"text-content-muted\">Auto-Link Confidence:</span> <span class=\"text-content\">")
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(book.ConfidenceScore) templ_7745c5c3_Var8, 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: 100, 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_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, "% confidence</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "% 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, 22, "</div><div class=\"mb-4\"><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, 23, "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("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, 24, "Manually Link</button></div><div id=\"matches-{ book.ProgressID }\" class=\"hidden\"><div class=\"border-t border-line pt-4\"><h4 class=\"font-semibold mb-3 text-content\">Potential Matches</h4><div id=\"matches-list-{ book.ProgressID }\" class=\"space-y-3\"><p class=\"text-content-muted\">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, 25, "</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: 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 text-content\">Manually Link Book</h2><button @click=\"hideManualLinkModal()\" class=\"icon-btn\">")
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, 26, "</button></div><input type=\"hidden\" id=\"link-progress-id\"> <input type=\"hidden\" id=\"link-book-sha256\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Book Title from Device</label> <input type=\"text\" id=\"link-book-title\" readonly class=\"input w-full text-content-muted\"></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">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\">Search</button></div></div><div id=\"link-search-results\" class=\"mb-4 max-h-48 overflow-y-auto space-y-2\"><p class=\"text-content-muted\">Search for books to link</p></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide text-content-muted mb-2\">Confidence Score</label> <input type=\"number\" id=\"link-confidence\" min=\"0\" max=\"1\" step=\"0.1\" value=\"1.0\" class=\"input w-full\"><p class=\"text-xs mt-1 text-content-muted\">1.0 = manual override (highest confidence)</p></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideManualLinkModal()\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" @click=\"confirmManualLink()\" class=\"btn btn-primary\">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
} }