feat(frontend): Convert admin library to SSR with TypeScript
Complete refactor of /admin/library page to use server-side rendering and TypeScript, fixing broken Create Library button and improving UX. Backend Integration: - Update template signature to accept libraries and users parameters - Add SSR rendering of library list (no empty state) - Add SSR rendering of user select dropdown - Data fetched in router handler and passed to template TypeScript Conversion: - Create web/src/library.ts (394 lines) - complete rewrite of inline JS - Convert all inline JavaScript to TypeScript - Fix critical data.data API response bug - Replace broken HTMX form with fetch() API calls - Implement event delegation for dynamic button clicks - Add missing editLibrary() function (placeholder) - Add proper error handling with toast notifications Template Changes: - Remove 229 lines of inline JavaScript - Update Create Library button: onclick → data-action - Update form: remove HTMX attributes, add onsubmit handler - Remove duplicate script tags from <head> section - Keep all script loading at end of <body> for performance Bug Fixes: - Fix API response handling: data → data.data - Replace broken hx-headers (JavaScript not supported in HTMX) - Fix modal z-index and visibility classes Progressive Enhancement: - Page works without JavaScript (SSR content visible) - JavaScript enhances with interactive features - Follows PROJECT_GUIDELINES: procedural style, no OOP
This commit is contained in:
1 parent
f096b86032
commit
638f1a901d
2 files changed
+154
-247
No files matched your search
+49
-244
@@ -1,13 +1,11 @@
|
||||
package templates
|
||||
|
||||
templ AdminLibrary(user User) {
|
||||
templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Library Management - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/toast.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="theme-tokyo-night">
|
||||
@@ -38,7 +36,7 @@ templ AdminLibrary(user User) {
|
||||
<a href="/admin" class="btn-secondary px-4 py-2 rounded-lg font-medium">
|
||||
← Back to Dashboard
|
||||
</a>
|
||||
<button onclick="showCreateLibraryModal()" class="btn-primary px-4 py-2 rounded-lg font-medium">
|
||||
<button data-action="show-create-modal" class="btn-primary px-4 py-2 rounded-lg font-medium">
|
||||
+ Create Library
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,7 +51,33 @@ templ AdminLibrary(user User) {
|
||||
<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">
|
||||
<!-- Libraries will be loaded here -->
|
||||
if len(libraries) == 0 {
|
||||
<p style="color: var(--text-secondary)" class="text-center py-8">
|
||||
No libraries yet. Create your first library to get started.
|
||||
</p>
|
||||
} else {
|
||||
for _, library := range libraries {
|
||||
<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)">{ library.Name }</h4>
|
||||
if library.Description != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ library.Description }</p>
|
||||
}
|
||||
<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">
|
||||
{ library.TypeName }
|
||||
</span>
|
||||
</div>
|
||||
<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="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="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="library-folders-{ library.ID }" class="hidden mt-3 space-y-2"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +100,9 @@ templ AdminLibrary(user User) {
|
||||
<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>
|
||||
for _, user := range users {
|
||||
<option value="{ user.ID }">{ user.Username } ({ user.Email })</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -88,264 +115,42 @@ templ AdminLibrary(user User) {
|
||||
</div>
|
||||
|
||||
<!-- Create Library Modal -->
|
||||
<div id="create-library-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
|
||||
<div class="card p-6 rounded-lg border max-w-md w-full mx-4" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Create Library</h3>
|
||||
<form hx-post="/api/libraries" hx-target="#create-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' onsubmit="this.reset();">
|
||||
<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">
|
||||
<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" 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="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" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"></textarea>
|
||||
<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" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||
<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" onclick="hideCreateLibraryModal()" class="btn-secondary px-4 py-2 rounded">Cancel</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded">Create</button>
|
||||
<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 id="create-result" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let libraries = [];
|
||||
let users = [];
|
||||
let libraryTypes = [];
|
||||
|
||||
function loadLibraries() {
|
||||
fetch('/api/libraries', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
libraries = data;
|
||||
renderLibraries();
|
||||
})
|
||||
.catch(error => console.error('Error loading libraries:', error));
|
||||
}
|
||||
|
||||
function renderLibraries() {
|
||||
const container = document.getElementById('libraries-list');
|
||||
container.innerHTML = libraries.map(library =>
|
||||
'<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)">' + library.name + '</h4>' +
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">' + (library.description || '') + '</p>' +
|
||||
'<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">' +
|
||||
(library.type_name || library.type) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex space-x-2">' +
|
||||
'<button onclick="showLibraryFolders(\'' + library.id + '\')" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>' +
|
||||
'<button onclick="editLibrary(\'' + library.id + '\')" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>' +
|
||||
'<button onclick="deleteLibrary(\'' + library.id + '\')" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="library-folders-' + library.id + '" class="hidden mt-3 space-y-2"></div>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function loadUsers() {
|
||||
fetch('/api/auth/users', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
users = data;
|
||||
renderUserSelect();
|
||||
})
|
||||
.catch(error => console.error('Error loading users:', error));
|
||||
}
|
||||
|
||||
function renderUserSelect() {
|
||||
const select = document.getElementById('user-select');
|
||||
select.innerHTML = '<option value="">Select a user...</option>' +
|
||||
users.map(user => '<option value="' + user.id + '">' + user.username + ' (' + user.email + ')</option>').join('');
|
||||
}
|
||||
|
||||
function loadUserVisibility() {
|
||||
const userId = document.getElementById('user-select').value;
|
||||
if (!userId) {
|
||||
document.getElementById('user-libraries').innerHTML = '<p style="color: var(--text-secondary)">Please select a user</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Load user's visible libraries
|
||||
fetch('/api/libraries/visible', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(visibleLibraries => {
|
||||
const container = document.getElementById('user-libraries');
|
||||
const visibleIds = visibleLibraries.map(lib => lib.id);
|
||||
|
||||
container.innerHTML = libraries.map(library => {
|
||||
const isVisible = visibleIds.includes(library.id);
|
||||
return '<label class="flex items-center space-x-3 p-2 rounded" style="background-color: var(--bg-primary);">' +
|
||||
'<input type="checkbox" ' + (isVisible ? 'checked' : '') +
|
||||
' onchange="setLibraryVisibility(\'' + userId + '\', \'' + library.id + '\', this.checked)" ' +
|
||||
'class="w-4 h-4">' +
|
||||
'<span style="color: var(--text-primary)">' + library.name + ' (' + library.type_name + ')</span>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
})
|
||||
.catch(error => console.error('Error loading user libraries:', error));
|
||||
}
|
||||
|
||||
function setLibraryVisibility(userId, libraryId, isVisible) {
|
||||
fetch('/api/libraries/visibility', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
library_id: libraryId,
|
||||
is_visible: isVisible
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(() => {
|
||||
// Refresh visibility controls
|
||||
loadUserVisibility();
|
||||
})
|
||||
.catch(error => console.error('Error setting library visibility:', error));
|
||||
}
|
||||
|
||||
function showCreateLibraryModal() {
|
||||
document.getElementById('create-library-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideCreateLibraryModal() {
|
||||
document.getElementById('create-library-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function deleteLibrary(libraryId) {
|
||||
const library = libraries.find(l => l.id === libraryId);
|
||||
if (!library) return;
|
||||
|
||||
if (confirm('Delete library "' + library.name + '"? This will permanently remove all associated media and cannot be undone.')) {
|
||||
fetch('/api/libraries/' + libraryId, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
loadLibraries(); // Refresh
|
||||
} else {
|
||||
alert('Error deleting library');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error deleting library:', error));
|
||||
}
|
||||
}
|
||||
|
||||
function showLibraryFolders(libraryId) {
|
||||
const container = document.getElementById('library-folders-' + libraryId);
|
||||
const library = libraries.find(l => l.id === libraryId);
|
||||
|
||||
if (container.classList.contains('hidden')) {
|
||||
// Load folders for this library
|
||||
fetch('/api/libraries/' + libraryId + '/folders', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(folders => {
|
||||
container.innerHTML = folders.map(folder =>
|
||||
'<div class="flex justify-between items-center p-2 rounded" style="background-color: var(--bg-secondary); border-color: var(--border)">' +
|
||||
'<span class="text-sm" style="color: var(--text-primary)">' + folder.folder_path + '</span>' +
|
||||
'<button onclick="removeLibraryFolder(\'' + libraryId + '\', \'' + folder.folder_path + '\')" ' +
|
||||
'class="text-xs text-red-500">Remove</button>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
// Add folder input
|
||||
container.innerHTML += '<div class="mt-2 flex space-x-2">' +
|
||||
'<input type="text" id="folder-path-' + libraryId + '" placeholder="Add folder path" ' +
|
||||
'class="flex-1 px-2 py-1 text-sm border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">' +
|
||||
'<button onclick="addLibraryFolder(\'' + libraryId + '\')" ' +
|
||||
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
||||
'</div>';
|
||||
|
||||
container.classList.remove('hidden');
|
||||
})
|
||||
.catch(error => console.error('Error loading folders:', error));
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function addLibraryFolder(libraryId) {
|
||||
const input = document.getElementById('folder-path-' + libraryId);
|
||||
const folderPath = input.value.trim();
|
||||
|
||||
if (!folderPath) return;
|
||||
|
||||
fetch('/api/libraries/' + libraryId + '/folders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ folder_path: folderPath })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(() => {
|
||||
input.value = '';
|
||||
showLibraryFolders(libraryId); // Refresh
|
||||
})
|
||||
.catch(error => console.error('Error adding folder:', error));
|
||||
}
|
||||
|
||||
function removeLibraryFolder(libraryId, folderPath) {
|
||||
if (confirm('Remove this folder from the library?')) {
|
||||
fetch('/api/libraries/' + libraryId + '/folders', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ folder_path: folderPath })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(() => {
|
||||
showLibraryFolders(libraryId); // Refresh
|
||||
})
|
||||
.catch(error => console.error('Error removing folder:', error));
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
// Load saved theme if exists
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
document.body.className = 'theme-' + savedTheme;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTheme();
|
||||
loadLibraries();
|
||||
loadUsers();
|
||||
});
|
||||
</script>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/toast.js"></script>
|
||||
<script src="/static/api.js"></script>
|
||||
<script src="/static/theme.js"></script>
|
||||
<script src="/static/library.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func AdminLibrary(user User) templ.Component {
|
||||
func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -29,7 +29,7 @@ func AdminLibrary(user User) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Library Management - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Library Management - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -37,7 +37,109 @@ func AdminLibrary(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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)\"><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\"><a href=\"/admin\" class=\"block px-4 py-2 rounded-lg hover:opacity-80\" style=\"color: var(--text-primary)\">🏠 Dashboard</a> <a href=\"/admin/profile\" class=\"block px-4 py-2 rounded-lg hover:opacity-80\" style=\"color: var(--text-primary)\">👤 Profile Settings</a> <a href=\"/admin/library\" class=\"block px-4 py-2 rounded-lg bg-accent text-bg-primary\" style=\"color: var(--text-primary)\">📚 Library Management</a></nav></div></aside><main class=\"flex-1 p-8\"><div class=\"max-w-6xl\"><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 onclick=\"showCreateLibraryModal()\" 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\"><!-- Libraries will be loaded here --></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></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=\"fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50\"><div class=\"card p-6 rounded-lg border max-w-md w-full mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border)\"><h3 class=\"text-xl font-semibold mb-4\" style=\"color: var(--text-primary)\">Create Library</h3><form hx-post=\"/api/libraries\" hx-target=\"#create-result\" hx-swap=\"innerHTML\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' onsubmit=\"this.reset();\"><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\" 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\" 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\" 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\" onclick=\"hideCreateLLine truncated
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex min-h-screen\" style=\"background-color: var(--bg-primary)\"><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\"><a href=\"/admin\" class=\"block px-4 py-2 rounded-lg hover:opacity-80\" style=\"color: var(--text-primary)\">🏠 Dashboard</a> <a href=\"/admin/profile\" class=\"block px-4 py-2 rounded-lg hover:opacity-80\" style=\"color: var(--text-primary)\">👤 Profile Settings</a> <a href=\"/admin/library\" class=\"block px-4 py-2 rounded-lg bg-accent text-bg-primary\" style=\"color: var(--text-primary)\">📚 Library Management</a></nav></div></aside><main class=\"flex-1 p-8\"><div class=\"max-w-6xl\"><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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(libraries) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p style=\"color: var(--text-secondary)\" class=\"text-center py-8\">No libraries yet. Create your first library to get started.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
for _, library := range libraries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<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)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 63, Col: 127}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</h4>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if library.Description != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 65, Col: 133}
|
||||
}
|
||||
_, 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, 7, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"inline-block px-2 py-1 text-xs rounded\" style=\"background-color: var(--accent); color: var(--bg-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 68, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span></div><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=\"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=\"delete\" class=\"text-xs px-2 py-1 rounded text-red-500\">Delete</button></div></div><div id=\"library-folders-{ library.ID }\" class=\"hidden mt-3 space-y-2\"></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</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> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, user := range users {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<option value=\"{ user.ID }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 104, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " (")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 104, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ")</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</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\"><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><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/api.js\"></script><script src=\"/static/theme.js\"></script><script src=\"/static/library.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user