diff options
| author | Bobby <[email protected]> | 2026-03-10 15:06:05 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-10 15:06:05 +0530 |
| commit | d6dfa963ad5b889a5828f14fdd169960642e0c6f (patch) | |
| tree | ff3826f2925919b70041a085a724290ea5fd1cca | |
| parent | c8d898abae7db1c6f8a7a52e106c93308cb55395 (diff) | |
| download | pagoda-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.tsx | 37 | ||||
| -rw-r--r-- | garden/src/pages/council/auditdetail.tsx | 15 | ||||
| -rw-r--r-- | garden/src/pages/council/auditlog.tsx | 33 | ||||
| -rw-r--r-- | garden/src/pages/council/bannedips.tsx | 14 | ||||
| -rw-r--r-- | garden/src/pages/council/user.tsx | 39 | ||||
| -rw-r--r-- | garden/src/pages/council/users.tsx | 36 | ||||
| -rw-r--r-- | garden/src/utils/api.ts | 3 | ||||
| -rw-r--r-- | garden/src/utils/format.ts | 16 | ||||
| -rw-r--r-- | garden/src/utils/status.ts | 9 |
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 |
