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
130 lines
4.9 KiB
Templ
130 lines
4.9 KiB
Templ
package templates
|
|
|
|
templ AdminUsers(currentUser User, users []User, adminCount int) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>Users - Bookhoard Admin</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body class="theme-{ currentUser.Theme }">
|
|
@Header(currentUser, "/admin/users")
|
|
<!-- Modal Container (populated by HTMX) -->
|
|
<div id="modal-container"></div>
|
|
<div class="flex min-h-screen bg-surface">
|
|
@AdminSidebar(currentUser, "/admin/users")
|
|
<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">
|
|
for _, user := range users {
|
|
<tr id={ "user-" + user.ID } 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">{ user.Username }</div>
|
|
if user.ID == currentUser.ID {
|
|
<span class="badge bg-brand text-white">You</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<!-- Email -->
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-content">{ user.Email }</div>
|
|
</td>
|
|
<!-- Role Toggle (with last-admin protection) -->
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
if user.Role == "admin" && adminCount == 1 {
|
|
<!-- Last admin - disabled -->
|
|
<div class="relative">
|
|
<select
|
|
disabled
|
|
class="input 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>
|
|
} else {
|
|
<select
|
|
hx-put={ "/api/auth/profile/" + user.ID }
|
|
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
|
hx-target={ "#role-result-" + user.ID }
|
|
hx-swap="innerHTML"
|
|
name="role"
|
|
class="input text-sm"
|
|
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
|
hx-trigger="change"
|
|
hx-vals='{"role": this.value}'
|
|
>
|
|
<option value="user" selected?={ user.Role == "user" }>User</option>
|
|
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
|
</select>
|
|
<div id={ "role-result-" + user.ID } class="text-xs mt-1"></div>
|
|
}
|
|
</td>
|
|
<!-- Created -->
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-content-muted">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
|
|
</td>
|
|
<!-- Actions -->
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<button
|
|
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
|
|
hx-target="#modal-container"
|
|
hx-swap="innerHTML"
|
|
class="btn btn-primary text-sm mr-2"
|
|
>
|
|
Edit
|
|
</button>
|
|
if user.Role == "admin" && adminCount == 1 {
|
|
<button
|
|
disabled
|
|
class="btn btn-danger text-sm cursor-not-allowed opacity-50"
|
|
title="Cannot delete the last admin"
|
|
>
|
|
Delete
|
|
</button>
|
|
} else {
|
|
<button
|
|
hx-delete={ "/api/auth/profile/" + user.ID }
|
|
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
|
hx-target={ "#user-" + user.ID }
|
|
hx-swap="outerHTML swap:0.5s"
|
|
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
|
class="btn btn-danger text-sm"
|
|
>
|
|
Delete
|
|
</button>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
}
|