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
- 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
195 lines
No EOL
6.3 KiB
JavaScript
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 · 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>
|
|
);
|
|
} |