Files
bookhoard/templates/admin_users.templ
T
john-okeefe 8ea70ea7f8 feat(ui): migrate remaining pages to the new shell
Apply the sidebar shell and bold primitives across the rest of the app so
the whole experience shares one visual language:

- Browse/organize: series (keeps stacked-covers/series-card CSS),
  collections (keeps wood-paneling + carousel classes), browse_detail.
- Account/stats: profile + form + modal, progress, analytics (stat-card
  tiles), devices, conflicts (status semantics), queue (status/priority
  badges).
- Admin: admin sidebar restyled with icons, dashboard/users/library/
  processing-issues/settings migrated to .card/.btn/.input primitives.
- Docs/setup/misc: docs (keeps prose/highlight.js), api_explorer, setup
  wizard, unlinked_books, custom_section builder.
- Modals/fragments: collection_modal, collection_rules,
  restore_system_collection_modal, filter_item, book_detail_modals — all
  overlays use --surface-overlay + --shadow-pop.

Every Alpine handler, HTMX attribute, id, name, and data-* is preserved;
only presentation changes.
2026-08-05 16:45:02 -04:00

145 lines
6.1 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">
@AdminSidebar(currentUser, "/admin/users")
<main class="flex-1 p-8">
<div class="max-w-5xl">
<div class="mb-8">
<div class="flex items-center gap-3 mb-1">
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("users", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">User Management</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Manage user accounts and permissions</p>
</div>
<!-- Users Table -->
<div class="card overflow-hidden">
<table class="w-full">
<thead style="background-color: var(--bg-primary)">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Username</th>
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Email</th>
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Role</th>
<th class="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Created</th>
<th class="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide" 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="transition-colors hover:bg-surface-hover">
<!-- Username -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-2">
<span class="grid place-items-center h-8 w-8 rounded-full shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("user", "h-4 w-4")
</span>
<div>
<div class="text-sm font-medium" style="color: var(--text-primary)">{ user.Username }</div>
if user.ID == currentUser.ID {
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">You</span>
}
</div>
</div>
</td>
<!-- Email -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm" style="color: var(--text-primary)">{ 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 w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed"
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 w-auto py-1 pr-7 text-xs"
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" style="color: var(--text-secondary)">{ FormatInTimezone(user.CreatedAt, currentUser.Timezone) }</div>
</td>
<!-- Actions -->
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="inline-flex items-center gap-1">
<button
hx-get={ "/admin/users/" + user.ID + "/profile-modal" }
hx-target="#modal-container"
hx-swap="innerHTML"
class="btn btn-secondary text-xs px-2.5 py-1"
>
@Icon("edit", "h-4 w-4")
Edit
</button>
if user.Role == "admin" && adminCount == 1 {
<button
disabled
class="btn btn-danger text-xs px-2.5 py-1"
title="Cannot delete the last admin"
>
@Icon("trash", "h-4 w-4")
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-xs px-2.5 py-1"
>
@Icon("trash", "h-4 w-4")
Delete
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</main>
</div>
</body>
</html>
}