release: 2901262102

This commit is contained in:
2026-01-29 21:03:32 +13:00
parent 914f478178
commit 06e0797722
6 changed files with 317 additions and 2 deletions

View File

@@ -0,0 +1,172 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
import AdminShell from '../../ui/AdminShell'
type RequestRow = {
id: number
title?: string | null
year?: number | null
type?: string | null
statusLabel?: string | null
requestedBy?: string | null
createdAt?: string | null
}
const formatDateTime = (value?: string | null) => {
if (!value) return 'Unknown'
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return value
return date.toLocaleString()
}
export default function AdminRequestsAllPage() {
const router = useRouter()
const [rows, setRows] = useState<RequestRow[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [pageSize, setPageSize] = useState(50)
const [page, setPage] = useState(1)
const pageCount = useMemo(() => {
if (!total || pageSize <= 0) return 1
return Math.max(1, Math.ceil(total / pageSize))
}, [total, pageSize])
const load = async () => {
if (!getToken()) {
router.push('/login')
return
}
setLoading(true)
setError(null)
try {
const baseUrl = getApiBase()
const skip = (page - 1) * pageSize
const response = await authFetch(
`${baseUrl}/admin/requests/all?take=${pageSize}&skip=${skip}`
)
if (!response.ok) {
if (response.status === 401) {
clearToken()
router.push('/login')
return
}
if (response.status === 403) {
router.push('/')
return
}
throw new Error(`Load failed: ${response.status}`)
}
const data = await response.json()
setRows(Array.isArray(data?.results) ? data.results : [])
setTotal(Number(data?.total ?? 0))
} catch (err) {
console.error(err)
setError('Unable to load requests.')
} finally {
setLoading(false)
}
}
useEffect(() => {
void load()
}, [page, pageSize])
useEffect(() => {
if (page > pageCount) {
setPage(pageCount)
}
}, [pageCount, page])
return (
<AdminShell
title="All requests"
subtitle="Paginated view of every cached request."
actions={
<button type="button" onClick={() => router.push('/admin')}>
Back to settings
</button>
}
>
<section className="admin-section">
<div className="admin-toolbar">
<div className="admin-toolbar-info">
<span>{total.toLocaleString()} total</span>
</div>
<div className="admin-toolbar-actions">
<label className="admin-select">
<span>Per page</span>
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
</label>
</div>
</div>
{loading ? (
<div className="status-banner">Loading requests</div>
) : error ? (
<div className="error-banner">{error}</div>
) : rows.length === 0 ? (
<div className="status-banner">No requests found.</div>
) : (
<div className="admin-table">
<div className="admin-table-head">
<span>Request</span>
<span>Status</span>
<span>Requested by</span>
<span>Created</span>
</div>
{rows.map((row) => (
<button
key={row.id}
type="button"
className="admin-table-row"
onClick={() => router.push(`/requests/${row.id}`)}
>
<span>
{row.title || `Request #${row.id}`}
{row.year ? ` (${row.year})` : ''}
</span>
<span>{row.statusLabel || 'Unknown'}</span>
<span>{row.requestedBy || 'Unknown'}</span>
<span>{formatDateTime(row.createdAt)}</span>
</button>
))}
</div>
)}
<div className="admin-pagination">
<button type="button" onClick={() => setPage(1)} disabled={page <= 1}>
First
</button>
<button type="button" onClick={() => setPage(page - 1)} disabled={page <= 1}>
Previous
</button>
<span>
Page {page} of {pageCount}
</span>
<button
type="button"
onClick={() => setPage(page + 1)}
disabled={page >= pageCount}
>
Next
</button>
<button
type="button"
onClick={() => setPage(pageCount)}
disabled={page >= pageCount}
>
Last
</button>
</div>
</section>
</AdminShell>
)
}

View File

@@ -1027,6 +1027,85 @@ button span {
gap: 12px;
}
.admin-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.admin-toolbar-info {
color: var(--ink-muted);
font-size: 13px;
}
.admin-toolbar-actions {
display: flex;
gap: 12px;
align-items: center;
}
.admin-select {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--ink-muted);
font-size: 13px;
}
.admin-table {
display: grid;
gap: 8px;
}
.admin-table-head {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 12px;
font-size: 12px;
color: var(--ink-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0 12px;
}
.admin-table-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 12px;
align-items: center;
text-align: left;
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.admin-table-row:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(15, 20, 45, 0.18);
}
.admin-pagination {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: flex-end;
color: var(--ink-muted);
font-size: 13px;
}
.admin-pagination button {
background: rgba(255, 255, 255, 0.08);
color: var(--ink);
}
.admin-pagination span {
padding: 0 6px;
}
.section-header {
display: flex;
justify-content: space-between;

View File

@@ -18,6 +18,7 @@ const NAV_GROUPS = [
title: 'Requests',
items: [
{ href: '/admin/requests', label: 'Request sync' },
{ href: '/admin/requests-all', label: 'All requests' },
{ href: '/admin/cache', label: 'Cache Control' },
],
},