summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-10 15:06:05 +0530
committerBobby <[email protected]>2026-03-10 15:06:05 +0530
commitd6dfa963ad5b889a5828f14fdd169960642e0c6f (patch)
treeff3826f2925919b70041a085a724290ea5fd1cca
parentc8d898abae7db1c6f8a7a52e106c93308cb55395 (diff)
downloadpagoda-d6dfa963ad5b889a5828f14fdd169960642e0c6f.tar.xz
pagoda-d6dfa963ad5b889a5828f14fdd169960642e0c6f.zip
feat: enhance audit and user management with pagination and improved date formatting
-rw-r--r--garden/src/components/Pagination.tsx37
-rw-r--r--garden/src/pages/council/auditdetail.tsx15
-rw-r--r--garden/src/pages/council/auditlog.tsx33
-rw-r--r--garden/src/pages/council/bannedips.tsx14
-rw-r--r--garden/src/pages/council/user.tsx39
-rw-r--r--garden/src/pages/council/users.tsx36
-rw-r--r--garden/src/utils/api.ts3
-rw-r--r--garden/src/utils/format.ts16
-rw-r--r--garden/src/utils/status.ts9
9 files changed, 96 insertions, 106 deletions
diff --git a/garden/src/components/Pagination.tsx b/garden/src/components/Pagination.tsx
new file mode 100644
index 0000000..f58e56a
--- /dev/null
+++ b/garden/src/components/Pagination.tsx
@@ -0,0 +1,37 @@
+import { Show } from "solid-js";
+
+interface PaginationProps {
+ page: number;
+ totalPages: number;
+ total: number;
+ label: string;
+ onPage: (page: number) => void;
+}
+
+export default function Pagination(props: PaginationProps) {
+ return (
+ <Show when={props.totalPages > 1}>
+ <div class="council-pagination">
+ <button
+ type="button"
+ class="council-page-btn"
+ disabled={props.page <= 1}
+ onClick={() => props.onPage(props.page - 1)}
+ >
+ Prev
+ </button>
+ <span class="council-page-info">
+ Page {props.page} of {props.totalPages} ({props.total} {props.label})
+ </span>
+ <button
+ type="button"
+ class="council-page-btn"
+ disabled={props.page >= props.totalPages}
+ onClick={() => props.onPage(props.page + 1)}
+ >
+ Next
+ </button>
+ </div>
+ </Show>
+ );
+} \ No newline at end of file
diff --git a/garden/src/pages/council/auditdetail.tsx b/garden/src/pages/council/auditdetail.tsx
index d259b56..bd4c188 100644
--- a/garden/src/pages/council/auditdetail.tsx
+++ b/garden/src/pages/council/auditdetail.tsx
@@ -4,6 +4,8 @@ import { api } from "../../api";
import { auth } from "../../store/auth";
import type { AuditLogDetail } from "../../types/admin";
import { AUDIT_ACTION_LABELS } from "../../types/admin";
+import { ROLE_LABELS } from "../../types/roles";
+import { formatDateTimeFull } from "../../utils/format";
import StaffGuard from "../../components/StaffGuard";
interface FieldChange {
@@ -28,11 +30,6 @@ export default function CouncilAuditDetail() {
}
});
- function formatDate(date: string) {
- const d = new Date(date);
- return d.toLocaleDateString() + " " + d.toLocaleTimeString();
- }
-
function actionLabel(action: string) {
return AUDIT_ACTION_LABELS[action] || action;
}
@@ -96,7 +93,7 @@ export default function CouncilAuditDetail() {
<Show when={data.disabled_until}>
<div class="council-detail-row">
<span class="council-detail-label">Until</span>
- <span class="council-detail-value">{formatDate(data.disabled_until!)}</span>
+ <span class="council-detail-value">{formatDateTimeFull(data.disabled_until!)}</span>
</div>
</Show>
<div class="council-detail-row">
@@ -113,13 +110,13 @@ export default function CouncilAuditDetail() {
<div class="council-detail-row">
<span class="council-detail-label">Old Role</span>
<span class="council-detail-value">
- <span class={`council-role council-role-${data.old_role}`}>{data.old_role}</span>
+ <span class={`council-role council-role-${data.old_role}`}>{ROLE_LABELS[data.old_role] || data.old_role}</span>
</span>
</div>
<div class="council-detail-row">
<span class="council-detail-label">New Role</span>
<span class="council-detail-value">
- <span class={`council-role council-role-${data.new_role}`}>{data.new_role}</span>
+ <span class={`council-role council-role-${data.new_role}`}>{ROLE_LABELS[data.new_role] || data.new_role}</span>
</span>
</div>
</div>
@@ -216,7 +213,7 @@ export default function CouncilAuditDetail() {
</div>
<div class="council-detail-row">
<span class="council-detail-label">Date</span>
- <span class="council-detail-value">{formatDate(e().created_at)}</span>
+ <span class="council-detail-value">{formatDateTimeFull(e().created_at)}</span>
</div>
<div class="council-detail-row">
<span class="council-detail-label">Action</span>
diff --git a/garden/src/pages/council/auditlog.tsx b/garden/src/pages/council/auditlog.tsx
index f957b6b..e574339 100644
--- a/garden/src/pages/council/auditlog.tsx
+++ b/garden/src/pages/council/auditlog.tsx
@@ -3,6 +3,8 @@ import { useNavigate, useSearchParams } from "@solidjs/router";
import { council } from "../../store/council";
import type { AuditLogEntry } from "../../types/admin";
import { AUDIT_ACTION_LABELS, AUDIT_TARGET_LABELS } from "../../types/admin";
+import { formatDateTime } from "../../utils/format";
+import Pagination from "../../components/Pagination";
import StaffGuard from "../../components/StaffGuard";
export default function CouncilAuditLog() {
@@ -58,11 +60,6 @@ export default function CouncilAuditLog() {
council.loadAuditLogs(p);
}
- function formatDate(date: string) {
- const d = new Date(date);
- return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
- }
-
function actionLabel(action: string) {
return AUDIT_ACTION_LABELS[action] || action;
}
@@ -129,7 +126,7 @@ export default function CouncilAuditLog() {
<For each={council.auditLogs()}>
{(entry: AuditLogEntry) => (
<div class="council-grid-row" onClick={() => navigate(`/council/auditlog/${entry.system_ref}`)}>
- <span class="council-audit-date">{formatDate(entry.created_at)}</span>
+ <span class="council-audit-date">{formatDateTime(entry.created_at)}</span>
<span class="council-audit-action">{actionLabel(entry.action)}</span>
<span>{entry.actor}</span>
<span class="council-audit-target">
@@ -144,29 +141,7 @@ export default function CouncilAuditLog() {
</Show>
</div>
- <Show when={council.auditTotalPages() > 1}>
- <div class="council-pagination">
- <button
- type="button"
- class="council-page-btn"
- disabled={council.auditPage() <= 1}
- onClick={() => goToPage(council.auditPage() - 1)}
- >
- Prev
- </button>
- <span class="council-page-info">
- Page {council.auditPage()} of {council.auditTotalPages()} ({council.auditTotal()} entries)
- </span>
- <button
- type="button"
- class="council-page-btn"
- disabled={council.auditPage() >= council.auditTotalPages()}
- onClick={() => goToPage(council.auditPage() + 1)}
- >
- Next
- </button>
- </div>
- </Show>
+ <Pagination page={council.auditPage()} totalPages={council.auditTotalPages()} total={council.auditTotal()} label="entries" onPage={goToPage} />
</section>
</StaffGuard>
);
diff --git a/garden/src/pages/council/bannedips.tsx b/garden/src/pages/council/bannedips.tsx
index 40e4987..e3c48af 100644
--- a/garden/src/pages/council/bannedips.tsx
+++ b/garden/src/pages/council/bannedips.tsx
@@ -2,6 +2,8 @@ import { createSignal, onMount, Show, For } from "solid-js";
import { useSearchParams } from "@solidjs/router";
import { api } from "../../api";
import { auth } from "../../store/auth";
+import { formatDate } from "../../utils/format";
+import Pagination from "../../components/Pagination";
import StaffGuard from "../../components/StaffGuard";
import Modal from "../../components/Modal";
@@ -60,10 +62,6 @@ export default function BannedIPs() {
loadBans(p);
});
- function formatDate(date: string) {
- return new Date(date).toLocaleDateString();
- }
-
return (
<StaffGuard>
<section>
@@ -98,13 +96,7 @@ export default function BannedIPs() {
</Show>
</div>
- <Show when={totalPages() > 1}>
- <div class="council-pagination">
- <button class="council-page-btn" disabled={page() <= 1} onClick={() => loadBans(page() - 1)}>Prev</button>
- <span class="council-page-info">Page {page()} of {totalPages()} ({total()} bans)</span>
- <button class="council-page-btn" disabled={page() >= totalPages()} onClick={() => loadBans(page() + 1)}>Next</button>
- </div>
- </Show>
+ <Pagination page={page()} totalPages={totalPages()} total={total()} label="bans" onPage={loadBans} />
<Show when={confirmBan()}>
{(ban: () => IPBan) => (
<Modal title="Lift IP Ban" onClose={() => setConfirmBan(null)}>
diff --git a/garden/src/pages/council/user.tsx b/garden/src/pages/council/user.tsx
index 0b299a0..5fc9627 100644
--- a/garden/src/pages/council/user.tsx
+++ b/garden/src/pages/council/user.tsx
@@ -4,6 +4,9 @@ import { api, uploadFile } from "../../api";
import { auth } from "../../store/auth";
import { UserRole, ROLE_LABELS } from "../../types/roles";
import type { AdminUser } from "../../types/admin";
+import { formatDate } from "../../utils/format";
+import { statusBadge } from "../../utils/status";
+import { extractError } from "../../utils/api";
import Modal from "../../components/Modal";
import Editor from "../../components/Editor";
import StaffGuard from "../../components/StaffGuard";
@@ -72,20 +75,6 @@ export default function CouncilUser() {
return me?.role === UserRole.Owner || me?.role === UserRole.Admin;
}
- function statusBadge() {
- const target = user();
- if (!target) return "";
- if (target.account_banned) return "banned";
- if (target.account_disabled) return "disabled";
- if (!target.email_verified) return "unverified";
- return "active";
- }
-
- function formatDate(date: string | null) {
- if (!date) return "\u2014";
- return new Date(date).toLocaleDateString();
- }
-
function startEdit(field: string, value: string) {
setEditing((prev: Record<string, string>) => ({ ...prev, [field]: value }));
}
@@ -113,7 +102,7 @@ export default function CouncilUser() {
setUser(response.data);
cancelEdit(field);
} else {
- setActionError((response.data as unknown as { error: string }).error);
+ setActionError(extractError(response.data));
}
}
@@ -131,7 +120,7 @@ export default function CouncilUser() {
if (response.ok) {
setUser(response.data);
} else {
- setActionError((response.data as unknown as { error: string }).error);
+ setActionError(extractError(response.data));
}
}
@@ -150,7 +139,7 @@ export default function CouncilUser() {
setUser(response.data);
cancelEdit("role");
} else {
- setActionError((response.data as unknown as { error: string }).error);
+ setActionError(extractError(response.data));
}
}
@@ -178,7 +167,7 @@ export default function CouncilUser() {
setWarnTitle("");
setWarnBody("");
} else {
- setModalError((response.data as unknown as { error: string }).error);
+ setModalError(extractError(response.data));
}
}
@@ -204,7 +193,7 @@ export default function CouncilUser() {
setDisableReason("");
setDisableUntil("");
} else {
- setModalError((response.data as unknown as { error: string }).error);
+ setModalError(extractError(response.data));
}
}
@@ -226,7 +215,7 @@ export default function CouncilUser() {
setModal(null);
setBanReason("");
} else {
- setModalError((response.data as unknown as { error: string }).error);
+ setModalError(extractError(response.data));
}
}
@@ -243,7 +232,7 @@ export default function CouncilUser() {
if (response.ok) {
setUser(response.data);
} else {
- setActionError((response.data as unknown as { error: string }).error);
+ setActionError(extractError(response.data));
}
}
@@ -260,7 +249,7 @@ export default function CouncilUser() {
if (response.ok) {
setUser(response.data);
} else {
- setActionError((response.data as unknown as { error: string }).error);
+ setActionError(extractError(response.data));
}
}
@@ -288,7 +277,7 @@ export default function CouncilUser() {
setModal(null);
setJadeAmount("");
} else {
- setModalError((response.data as unknown as { error: string }).error);
+ setModalError(extractError(response.data));
}
}
@@ -530,7 +519,7 @@ export default function CouncilUser() {
const result = await uploadFile<{ url: string }>("/council/upload", file, auth.token()!);
setUploadingImage(false);
if (!result.ok) {
- setActionError((result.data as unknown as { error: string }).error);
+ setActionError(extractError(result.data));
return;
}
html += `<img src="${result.data.url}" alt="" class="signature-image" />`;
@@ -618,7 +607,7 @@ export default function CouncilUser() {
<div class="council-detail-card">
<img src={target().avatar_url} alt="" class="council-detail-card-avatar" />
<span class="council-detail-card-username">@{target().username}</span>
- <span class={`council-status council-status-${statusBadge()}`}>{statusBadge()}</span>
+ <span class={`council-status council-status-${statusBadge(target())}`}>{statusBadge(target())}</span>
<span class={`council-role council-role-${target().role}`}>{ROLE_LABELS[target().role] || target().role}</span>
</div>
</div>
diff --git a/garden/src/pages/council/users.tsx b/garden/src/pages/council/users.tsx
index e88ad85..97f6cd2 100644
--- a/garden/src/pages/council/users.tsx
+++ b/garden/src/pages/council/users.tsx
@@ -3,6 +3,9 @@ import { useNavigate, useSearchParams } from "@solidjs/router";
import { council } from "../../store/council";
import { ROLE_LABELS } from "../../types/roles";
import type { AdminUser } from "../../types/admin";
+import { formatDate } from "../../utils/format";
+import { statusBadge } from "../../utils/status";
+import Pagination from "../../components/Pagination";
import StaffGuard from "../../components/StaffGuard";
export default function CouncilUsers() {
@@ -27,17 +30,6 @@ export default function CouncilUsers() {
council.loadUsers(p, council.search());
}
- function statusBadge(user: AdminUser) {
- if (user.account_banned) return "banned";
- if (user.account_disabled) return "disabled";
- if (!user.email_verified) return "unverified";
- return "active";
- }
-
- function formatDate(date: string) {
- return new Date(date).toLocaleDateString();
- }
-
function sortIndicator(field: string) {
if (council.sortField() !== field) return "";
return council.sortOrder() === "asc" ? " \u25B2" : " \u25BC";
@@ -99,27 +91,7 @@ export default function CouncilUsers() {
</Show>
</div>
- <Show when={council.totalPages() > 1}>
- <div class="council-pagination">
- <button
- class="council-page-btn"
- disabled={council.page() <= 1}
- onClick={() => goToPage(council.page() - 1)}
- >
- Prev
- </button>
- <span class="council-page-info">
- Page {council.page()} of {council.totalPages()} ({council.total()} users)
- </span>
- <button
- class="council-page-btn"
- disabled={council.page() >= council.totalPages()}
- onClick={() => goToPage(council.page() + 1)}
- >
- Next
- </button>
- </div>
- </Show>
+ <Pagination page={council.page()} totalPages={council.totalPages()} total={council.total()} label="users" onPage={goToPage} />
</section>
</StaffGuard>
);
diff --git a/garden/src/utils/api.ts b/garden/src/utils/api.ts
new file mode 100644
index 0000000..cb560d5
--- /dev/null
+++ b/garden/src/utils/api.ts
@@ -0,0 +1,3 @@
+export function extractError(data: unknown): string {
+ return (data as { error: string }).error || "An error occurred.";
+} \ No newline at end of file
diff --git a/garden/src/utils/format.ts b/garden/src/utils/format.ts
new file mode 100644
index 0000000..fdf3336
--- /dev/null
+++ b/garden/src/utils/format.ts
@@ -0,0 +1,16 @@
+export function formatDate(date: string | null): string {
+ if (!date) return "\u2014";
+ return new Date(date).toLocaleDateString();
+}
+
+export function formatDateTime(date: string | null): string {
+ if (!date) return "\u2014";
+ const d = new Date(date);
+ return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+}
+
+export function formatDateTimeFull(date: string | null): string {
+ if (!date) return "\u2014";
+ const d = new Date(date);
+ return d.toLocaleDateString() + " " + d.toLocaleTimeString();
+} \ No newline at end of file
diff --git a/garden/src/utils/status.ts b/garden/src/utils/status.ts
new file mode 100644
index 0000000..68947b0
--- /dev/null
+++ b/garden/src/utils/status.ts
@@ -0,0 +1,9 @@
+import type { AdminUser } from "../types/admin";
+
+export function statusBadge(user: AdminUser | null): string {
+ if (!user) return "";
+ if (user.account_banned) return "banned";
+ if (user.account_disabled) return "disabled";
+ if (!user.email_verified) return "unverified";
+ return "active";
+} \ No newline at end of file