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:
+1
-17
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user