deckhearth/components/AdminCatalogPagination.js
Randall Stillwell bf317f819e feat(admin): add admin card catalog (Phase 1)
- 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
2026-09-18 14:06:01 -05:00

99 lines
No EOL
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}