- New admin API: /api/admin/cards with filters (game/rarity/set/price/verified), sorting, pagination, admin rate limiting - New admin page: /admin/catalog with GlassSurface table, filters panel, sortable columns, infinite scroll pagination - New hook: useAdminCatalog for state management - Components: AdminCatalogView, AdminCatalogFilters, AdminCatalogTable, AdminCatalogPagination - Protected by withAdmin middleware, reuses design system tokens
99 lines
No EOL
3.8 KiB
JavaScript
99 lines
No EOL
3.8 KiB
JavaScript
import { GlassSurface, Button, Input } from './ui';
|
||
|
||
export default function AdminCatalogPagination({
|
||
pagination,
|
||
hasMore,
|
||
loadingMore,
|
||
onLoadMore,
|
||
onLimitChange
|
||
}) {
|
||
const { page, limit, total, pages } = pagination;
|
||
|
||
if (total === 0) return null;
|
||
|
||
return (
|
||
<GlassSurface tint="mid" rim="subtle" className="mt-4 p-4">
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div className="flex flex-wrap items-center gap-4">
|
||
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||
Showing {(page - 1) * limit + 1}–{Math.min(page * limit, total)} of {total.toLocaleString()}
|
||
</span>
|
||
<div className="flex items-center gap-2">
|
||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Per page:</label>
|
||
<select
|
||
value={limit}
|
||
onChange={(e) => onLimitChange(parseInt(e.target.value))}
|
||
disabled={loadingMore}
|
||
className="glass-input rounded-xl px-3 py-1.5 text-sm"
|
||
style={{
|
||
border: '1px solid var(--border)',
|
||
color: 'var(--text-primary)',
|
||
backgroundColor: 'var(--bg-tertiary)',
|
||
appearance: 'none',
|
||
backgroundImage: "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e\")",
|
||
backgroundPosition: 'right 0.5rem center',
|
||
backgroundRepeat: 'no-repeat',
|
||
backgroundSize: '1.5em 1.5em',
|
||
paddingRight: '2rem',
|
||
minWidth: '80px'
|
||
}}
|
||
>
|
||
<option value="25">25</option>
|
||
<option value="50">50</option>
|
||
<option value="100">100</option>
|
||
<option value="200">200</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => onLoadMore()}
|
||
disabled={loadingMore || !hasMore}
|
||
leadingIcon={loadingMore && (
|
||
<span className="animate-spin inline-block h-4 w-4 border-2 border-current border-r-transparent rounded-full" />
|
||
)}
|
||
>
|
||
{hasMore ? 'Load More' : 'End of Results'}
|
||
</Button>
|
||
|
||
{pages > 1 && (
|
||
<>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => onLimitChange(limit)} // placeholder - page nav handled by load more
|
||
disabled={page === 1}
|
||
leadingIcon={
|
||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
}
|
||
>
|
||
Prev
|
||
</Button>
|
||
<span className="px-3 text-sm font-mono" style={{ color: 'var(--text-secondary)' }}>
|
||
Page {page} of {pages}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => onLoadMore()}
|
||
disabled={page === pages || loadingMore}
|
||
trailingIcon={
|
||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
}
|
||
>
|
||
Next
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</GlassSurface>
|
||
);
|
||
} |