refactor(admin): integrate reusable sidebar into admin pages

- Update admin.templ to use @AdminSidebar component
- Update admin_library.templ to use @AdminSidebar component
- Update admin_users.templ to use @AdminSidebar component
- Add sidebar layout wrapper to admin_users.templ (previously missing)
- Fix theme rendering to use user.Theme in all admin templates
- Remove duplicated sidebar markup across admin pages
This commit is contained in:
2026-02-24 10:24:50 -05:00
parent 3b0fee4281
commit 4be69861bd
3 changed files with 121 additions and 148 deletions
+1 -17
View File
@@ -14,23 +14,7 @@ templ Admin(user User) {
@Header(user, "/admin") @Header(user, "/admin")
<div class="flex min-h-screen" style="background-color: var(--bg-primary)"> <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)"> @AdminSidebar(user, "/admin")
<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 bg-accent text-bg-primary" style="color: var(--text-primary)">
🏠 Dashboard
</a>
<a href="/admin/users" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
👤 User Administration
</a>
<a href="/admin/library" class="block px-4 py-2 rounded-lg hover:opacity-80" style="color: var(--text-primary)">
📚 Library Management
</a>
</nav>
</div>
</aside>
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="max-w-4xl"> <div class="max-w-4xl">
<div class="mb-8"> <div class="mb-8">
+2 -17
View File
@@ -8,25 +8,10 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
<title>Library Management - Bookhoard</title> <title>Library Management - Bookhoard</title>
<link href="/static/style.css" rel="stylesheet"/> <link href="/static/style.css" rel="stylesheet"/>
</head> </head>
<body class="theme-tokyo-night"> <body 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" style="background-color: var(--bg-primary)">
<aside class="w-64 border-r" style="background-color: var(--bg-secondary); border-color: var(--border)"> @AdminSidebar(user, "/admin/library")
<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"> <main class="flex-1 p-8">
<div class="max-w-6xl"> <div class="max-w-6xl">
<div class="mb-8"> <div class="mb-8">
+118 -114
View File
@@ -12,130 +12,134 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
<script src="/static/theme.js"></script> <script src="/static/theme.js"></script>
<link href="/static/style.css" rel="stylesheet"> <link href="/static/style.css" rel="stylesheet">
</head> </head>
<body class="theme-tokyo-night"> <body class="theme-{ currentUser.Theme }">
@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>
<main class="max-w-7xl mx-auto px-4 py-8"> <div class="flex min-h-screen" style="background-color: var(--bg-primary)">
<div class="mb-8"> @AdminSidebar(currentUser, "/admin/users")
<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 --> <main class="flex-1 p-8">
<div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)"> <div class="max-w-7xl">
<table class="w-full"> <h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary)">User Management</h1>
<thead style="background-color: var(--bg-primary)"> <p style="color: var(--text-secondary)">Manage user accounts and permissions</p>
<tr> </div>
<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)">
for _, user := range users {
<tr id={ "user-" + user.ID } 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)">{user.Username}</div>
if user.ID == currentUser.ID {
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span>
}
</div>
</div>
</td>
<!-- Email --> <!-- Users Table -->
<td class="px-6 py-4 whitespace-nowrap"> <div class="card rounded-lg border overflow-hidden" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="text-sm" style="color: var(--text-primary)">{user.Email}</div> <table class="w-full">
</td> <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)">
for _, user := range users {
<tr id={ "user-" + user.ID } 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)">{user.Username}</div>
if user.ID == currentUser.ID {
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: white;">You</span>
}
</div>
</div>
</td>
<!-- Role Toggle (with last-admin protection) --> <!-- Email -->
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
if user.Role == "admin" && adminCount == 1 { <div class="text-sm" style="color: var(--text-primary)">{user.Email}</div>
<!-- Last admin - disabled --> </td>
<div class="relative">
<select
disabled
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
title="Cannot demote the last admin"
>
<option value="user">User</option>
<option value="admin" selected>Admin</option>
</select>
</div>
} else {
<select
hx-put={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#role-result-" + user.ID }
hx-swap="innerHTML"
name="role"
class="text-sm rounded px-2 py-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
hx-trigger="change"
hx-vals='{"role": this.value}'
>
<option value="user" selected?={user.Role == "user"}>User</option>
<option value="admin" selected?={user.Role == "admin"}>Admin</option>
</select>
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
}
</td>
<!-- Created --> <!-- Role Toggle (with last-admin protection) -->
<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)">{ user.CreatedAt.Format("2006-01-02") }</div> if user.Role == "admin" && adminCount == 1 {
</td> <!-- Last admin - disabled -->
<div class="relative">
<select
disabled
class="text-sm rounded px-2 py-1 cursor-not-allowed opacity-50"
style="background-color: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border);"
title="Cannot demote the last admin"
>
<option value="user">User</option>
<option value="admin" selected>Admin</option>
</select>
</div>
} else {
<select
hx-put={ "/api/auth/profile/" + user.ID }
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
hx-target={ "#role-result-" + user.ID }
hx-swap="innerHTML"
name="role"
class="text-sm rounded px-2 py-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border);"
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
hx-trigger="change"
hx-vals='{"role": this.value}'
>
<option value="user" selected?={user.Role == "user"}>User</option>
<option value="admin" selected?={user.Role == "admin"}>Admin</option>
</select>
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
}
</td>
<!-- Actions --> <!-- Created -->
<td class="px-6 py-4 whitespace-nowrap text-right"> <td class="px-6 py-4 whitespace-nowrap">
<button <div class="text-sm" style="color: var(--text-secondary)">{ user.CreatedAt.Format("2006-01-02") }</div>
hx-get={ "/admin/users/" + user.ID + "/profile-modal" } </td>
hx-target="#modal-container"
hx-swap="innerHTML" <!-- Actions -->
class="text-sm px-3 py-1 rounded mr-2" <td class="px-6 py-4 whitespace-nowrap text-right">
style="background-color: var(--accent); color: white;" <button
> hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
Edit hx-target="#modal-container"
</button> hx-swap="innerHTML"
if user.Role == "admin" && adminCount == 1 { class="text-sm px-3 py-1 rounded mr-2"
<button style="background-color: var(--accent); color: white;"
disabled >
class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50" Edit
style="background-color: #dc2626; color: white;" </button>
title="Cannot delete the last admin" if user.Role == "admin" && adminCount == 1 {
> <button
Delete disabled
</button> class="text-sm px-3 py-1 rounded cursor-not-allowed opacity-50"
} else { style="background-color: #dc2626; color: white;"
<button title="Cannot delete the last admin"
hx-delete={ "/api/auth/profile/" + user.ID } >
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' Delete
hx-target={ "#user-" + user.ID } </button>
hx-swap="outerHTML swap:0.5s" } else {
hx-confirm="Are you sure you want to delete this user? This action cannot be undone." <button
class="text-sm px-3 py-1 rounded" hx-delete={ "/api/auth/profile/" + user.ID }
style="background-color: #dc2626; color: white;" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
> hx-target={ "#user-" + user.ID }
Delete hx-swap="outerHTML swap:0.5s"
</button> hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
} class="text-sm px-3 py-1 rounded"
</td> style="background-color: #dc2626; color: white;"
</tr> >
} Delete
</tbody> </button>
</table> }
</div> </td>
</main> </tr>
}
</tbody>
</table>
</div>
</main>
</div>
</body> </body>
</html> </html>
} }