deckhearth/components/AdminCatalogView.js
Randall Stillwell 70806d81ec
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
feat(admin): add batch selection + batch edit (Phase 2)
- AdminBulkSelectionToolbar: floating toolbar with batch edit action + clear selection
- BatchEditModal: form for editing allowed fields (rarity, verified, game, mana_cost, cmc, card_type, edhrec_rank, layout, released_at, reserved, reprint) with live preview
- API: POST /api/admin/cards/batch with validation, allowlist, 1000-card cap, audit logging
- Hook: selection state (Set), select-all, batch edit workflow
- Table: checkbox column, row highlighting, select-all header
- Build: all routes compile, lint clean (pre-existing only), 342 tests pass
2026-09-18 14:57:14 -05:00

195 lines
No EOL
6.3 KiB
JavaScript

import { useState, useEffect } from 'react';
import { GlassSurface, Button, Input, SearchBar, StatCard, Modal } from './ui';
import AdminCatalogFilters from './AdminCatalogFilters';
import AdminCatalogTable from './AdminCatalogTable';
import AdminCatalogPagination from './AdminCatalogPagination';
import BulkSelectionToolbar from './BulkSelectionToolbar';
import AdminBulkSelectionToolbar from './AdminBulkSelectionToolbar';
import BatchEditModal from './BatchEditModal';
export default function AdminCatalogView(props) {
const {
cards,
loading,
loadingMore,
searchQuery,
selectedGame,
selectedRarity,
selectedSet,
minPrice,
maxPrice,
verifiedOnly,
sortColumn,
sortDirection,
pagination,
filters,
hasMore,
error,
handleSearchChange,
handleGameChange,
handleRarityChange,
handleSetChange,
handleMinPriceChange,
handleMaxPriceChange,
handleVerifiedChange,
handleSort,
handleLoadMore,
handleRefresh,
handleLimitChange,
// Selection
selectedCardIds,
selectedCount,
handleToggleSelect,
handleSelectAll,
handleClearSelection,
isSelected,
// Batch edit
batchEditModalOpen,
batchEditLoading,
batchEditError,
openBatchEditModal,
closeBatchEditModal,
executeBatchEdit
} = props;
const [showFilters, setShowFilters] = useState(true);
const cardCount = cards.length;
const totalCount = pagination.total;
const allSelected = selectedCount > 0 && selectedCount === cards.length;
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop >=
document.documentElement.offsetHeight - 1000
) {
handleLoadMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleLoadMore, hasMore, loadingMore]);
return (
<div className="min-h-screen" style={{ background: 'var(--bg-primary)' }}>
{/* Header */}
<div className="px-4 sm:px-6 lg:px-8 pt-6 pb-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
Card Catalog
</h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Admin view: {totalCount.toLocaleString()} cards total &middot; showing {cardCount.toLocaleString()}
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button
variant="secondary"
size="sm"
onClick={handleRefresh}
disabled={loading}
leadingIcon={
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
}
>
Refresh
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowFilters(!showFilters)}
leadingIcon={
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
}
>
{showFilters ? 'Hide Filters' : 'Show Filters'}
</Button>
</div>
</div>
{/* Bulk Selection Toolbar */}
{selectedCount > 0 && (
<AdminBulkSelectionToolbar
selectedCount={selectedCount}
onClearSelection={handleClearSelection}
onBatchEdit={openBatchEditModal}
/>
)}
{/* Filters Panel */}
{showFilters && (
<AdminCatalogFilters
searchQuery={searchQuery}
selectedGame={selectedGame}
selectedRarity={selectedRarity}
selectedSet={selectedSet}
minPrice={minPrice}
maxPrice={maxPrice}
verifiedOnly={verifiedOnly}
filters={filters}
onSearchChange={handleSearchChange}
onGameChange={handleGameChange}
onRarityChange={handleRarityChange}
onSetChange={handleSetChange}
onMinPriceChange={handleMinPriceChange}
onMaxPriceChange={handleMaxPriceChange}
onVerifiedChange={handleVerifiedChange}
loading={loading}
/>
)}
{/* Error Banner */}
{error && (
<GlassSurface tint="low" rim="ember-subtle" className="mt-4 p-4" style={{ background: 'rgba(220, 38, 38, 0.1)' }}>
<div className="flex items-center justify-between gap-4">
<span style={{ color: '#dc2626' }}>Error: {error}</span>
<Button variant="ghost" size="sm" onClick={handleRefresh}>Retry</Button>
</div>
</GlassSurface>
)}
</div>
{/* Table */}
<div className="px-4 sm:px-6 lg:px-8 pb-8">
<AdminCatalogTable
cards={cards}
loading={loading || loadingMore}
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
hasMore={hasMore}
loadingMore={loadingMore}
selectedCardIds={selectedCardIds}
onToggleSelect={handleToggleSelect}
onSelectAll={handleSelectAll}
allSelected={allSelected}
/>
{/* Pagination */}
<AdminCatalogPagination
pagination={pagination}
hasMore={hasMore}
loadingMore={loadingMore}
onLoadMore={handleLoadMore}
onLimitChange={handleLimitChange}
/>
</div>
{/* Batch Edit Modal */}
<BatchEditModal
isOpen={batchEditModalOpen}
onClose={closeBatchEditModal}
onSubmit={executeBatchEdit}
loading={batchEditLoading}
error={batchEditError}
selectedCount={selectedCount}
/>
</div>
);
}