diff --git a/components/AdminCatalogFilters.js b/components/AdminCatalogFilters.js
new file mode 100644
index 0000000..9e1ae10
--- /dev/null
+++ b/components/AdminCatalogFilters.js
@@ -0,0 +1,191 @@
+import { SearchBar, Input, GlassSurface, Button } from './ui';
+
+const GAME_OPTIONS = [
+ { value: 'all', label: 'All Games' },
+ { value: 'mtg', label: 'Magic: The Gathering' },
+ { value: 'pokemon', label: 'Pokémon TCG' },
+ { value: 'lorcana', label: 'Disney Lorcana' }
+];
+
+const RARITY_OPTIONS = [
+ { value: 'all', label: 'All Rarities' },
+ { value: 'common', label: 'Common' },
+ { value: 'uncommon', label: 'Uncommon' },
+ { value: 'rare', label: 'Rare' },
+ { value: 'mythic', label: 'Mythic' },
+ { value: 'special', label: 'Special' },
+ { value: 'bonus', label: 'Bonus' }
+];
+
+export default function AdminCatalogFilters({
+ searchQuery,
+ selectedGame,
+ selectedRarity,
+ selectedSet,
+ minPrice,
+ maxPrice,
+ verifiedOnly,
+ filters,
+ onSearchChange,
+ onGameChange,
+ onRarityChange,
+ onSetChange,
+ onMinPriceChange,
+ onMaxPriceChange,
+ onVerifiedChange,
+ loading
+}) {
+ return (
+
+
+
+ onSearchChange(e.target.value)}
+ placeholder="Search card name..."
+ disabled={loading}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminCatalogPagination.js b/components/AdminCatalogPagination.js
new file mode 100644
index 0000000..52b37a3
--- /dev/null
+++ b/components/AdminCatalogPagination.js
@@ -0,0 +1,99 @@
+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 (
+
+
+
+
+ Showing {(page - 1) * limit + 1}–{Math.min(page * limit, total)} of {total.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+ {pages > 1 && (
+ <>
+
+
+ Page {page} of {pages}
+
+
+ >
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminCatalogTable.js b/components/AdminCatalogTable.js
new file mode 100644
index 0000000..018f533
--- /dev/null
+++ b/components/AdminCatalogTable.js
@@ -0,0 +1,274 @@
+import { GlassSurface } from './ui';
+
+const SORTABLE_COLUMNS = [
+ { key: 'name', label: 'Name' },
+ { key: 'game', label: 'Game' },
+ { key: 'set_name', label: 'Set' },
+ { key: 'card_number', label: '#' },
+ { key: 'rarity', label: 'Rarity' },
+ { key: 'current_price', label: 'Current $' },
+ { key: 'market_price', label: 'Market $' },
+ { key: 'prices_updated_at', label: 'Price Updated' },
+ { key: 'verified', label: 'Verified' }
+];
+
+const COLUMNS = [
+ { key: 'name', label: 'Name', width: '35%' },
+ { key: 'game', label: 'Game', width: '8%' },
+ { key: 'set_name', label: 'Set', width: '15%' },
+ { key: 'card_number', label: '#', width: '5%' },
+ { key: 'rarity', label: 'Rarity', width: '8%' },
+ { key: 'current_price', label: 'Current $', width: '8%' },
+ { key: 'market_price', label: 'Market $', width: '8%' },
+ { key: 'prices_updated_at', label: 'Price Updated', width: '10%' },
+ { key: 'verified', label: 'Verified', width: '5%' }
+];
+
+function formatPrice(price) {
+ if (price === null || price === undefined || price === '') return '—';
+ const num = typeof price === 'string' ? parseFloat(price) : price;
+ if (isNaN(num)) return '—';
+ return `$${num.toFixed(2)}`;
+}
+
+function formatDate(dateStr) {
+ if (!dateStr) return '—';
+ try {
+ const date = new Date(dateStr);
+ if (isNaN(date.getTime())) return '—';
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+ } catch {
+ return '—';
+ }
+}
+
+function getRarityBadge(rarity) {
+ if (!rarity) return null;
+ const colors = {
+ common: { bg: 'var(--bg-tertiary)', text: 'var(--text-secondary)', border: 'var(--border)' },
+ uncommon: { bg: 'rgba(34, 197, 94, 0.15)', text: '#22c55e', border: 'rgba(34, 197, 94, 0.3)' },
+ rare: { bg: 'rgba(59, 130, 246, 0.15)', text: '#3b82f6', border: 'rgba(59, 130, 246, 0.3)' },
+ mythic: { bg: 'rgba(168, 85, 247, 0.15)', text: '#a855f7', border: 'rgba(168, 85, 247, 0.3)' },
+ special: { bg: 'rgba(249, 115, 22, 0.15)', text: '#f97316', border: 'rgba(249, 115, 22, 0.3)' },
+ bonus: { bg: 'rgba(234, 179, 8, 0.15)', text: '#eab308', border: 'rgba(234, 179, 8, 0.3)' }
+ };
+ const style = colors[rarity.toLowerCase()] || colors.common;
+ return (
+
+ {rarity.charAt(0).toUpperCase() + rarity.slice(1)}
+
+ );
+}
+
+function getGameBadge(game) {
+ if (!game) return null;
+ const styles = {
+ mtg: { bg: 'rgba(59, 130, 246, 0.15)', text: '#3b82f6', border: 'rgba(59, 130, 246, 0.3)', label: 'MTG' },
+ pokemon: { bg: 'rgba(249, 115, 22, 0.15)', text: '#f97316', border: 'rgba(249, 115, 22, 0.3)', label: 'Pokémon' },
+ lorcana: { bg: 'rgba(168, 85, 247, 0.15)', text: '#a855f7', border: 'rgba(168, 85, 247, 0.3)', label: 'Lorcana' }
+ };
+ const style = styles[game.toLowerCase()] || { bg: 'var(--bg-tertiary)', text: 'var(--text-secondary)', border: 'var(--border)', label: game };
+ return (
+
+ {style.label}
+
+ );
+}
+
+export default function AdminCatalogTable({
+ cards,
+ loading,
+ sortColumn,
+ sortDirection,
+ onSort,
+ hasMore,
+ loadingMore
+}) {
+ if (loading && cards.length === 0) {
+ return (
+
+
+
+
Loading card catalog...
+
+
+ );
+ }
+
+ if (cards.length === 0) {
+ return (
+
+
+
+
No cards found matching your filters.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {COLUMNS.map(col => {
+ const sortable = SORTABLE_COLUMNS.some(sc => sc.key === col.key);
+ return (
+ sortable && onSort(col.key)}
+ >
+
+ {col.label}
+ {sortable && sortColumn === col.key && (
+
+ )}
+
+ |
+ );
+ })}
+
+
+
+ {cards.map((card, index) => (
+
+
+
+ {card.image_url && (
+ // eslint-disable-next-line @next/next/no-img-element
+ 
+ )}
+
+
+ {card.name}
+
+
+ {card.set_code?.toUpperCase()} {card.card_number || ''}
+ {card.oracle_id && [{card.oracle_id.slice(0, 8)}]}
+
+
+
+ |
+
+ {getGameBadge(card.game)}
+ |
+
+
+ {card.set_name}
+
+ |
+
+
+ {card.card_number || '—'}
+
+ |
+
+ {getRarityBadge(card.rarity)}
+ |
+
+
+ {formatPrice(card.current_price)}
+
+ |
+
+
+ {formatPrice(card.market_price)}
+
+ |
+
+
+ {formatDate(card.prices_updated_at)}
+
+ |
+
+ {card.verified ? (
+
+ ) : (
+
+ )}
+ |
+
+ ))}
+
+
+
+
+ {loadingMore && hasMore && (
+
+
+
+
Loading more cards...
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminCatalogView.js b/components/AdminCatalogView.js
new file mode 100644
index 0000000..9c47ce1
--- /dev/null
+++ b/components/AdminCatalogView.js
@@ -0,0 +1,154 @@
+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';
+
+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
+ } = props;
+
+ const [showFilters, setShowFilters] = useState(true);
+
+ const cardCount = cards.length;
+ const totalCount = pagination.total;
+
+ 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 (
+
+ {/* Header */}
+
+
+
+
+ Card Catalog
+
+
+ Admin view: {totalCount.toLocaleString()} cards total · showing {cardCount.toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Filters Panel */}
+ {showFilters && (
+
+ )}
+
+ {/* Error Banner */}
+ {error && (
+
+
+ Error: {error}
+
+
+
+ )}
+
+
+ {/* Table */}
+
+
+
+ {/* Pagination */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/lib/use-admin-catalog.js b/lib/use-admin-catalog.js
new file mode 100644
index 0000000..7b3483a
--- /dev/null
+++ b/lib/use-admin-catalog.js
@@ -0,0 +1,224 @@
+import { useState, useEffect, useRef } from 'react';
+
+export function useAdminCatalog() {
+ const [cards, setCards] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedGame, setSelectedGame] = useState('all');
+ const [selectedRarity, setSelectedRarity] = useState('all');
+ const [selectedSet, setSelectedSet] = useState('all');
+ const [minPrice, setMinPrice] = useState('');
+ const [maxPrice, setMaxPrice] = useState('');
+ const [verifiedOnly, setVerifiedOnly] = useState(false);
+ const [sortColumn, setSortColumn] = useState('name');
+ const [sortDirection, setSortDirection] = useState('asc');
+ const [pagination, setPagination] = useState({
+ page: 1,
+ limit: 50,
+ total: 0,
+ pages: 0
+ });
+ const [filters, setFilters] = useState({
+ games: [],
+ rarities: [],
+ sets: []
+ });
+ const [hasMore, setHasMore] = useState(true);
+ const [error, setError] = useState(null);
+ const loadingMoreRef = useRef(false);
+ const hasMoreRef = useRef(true);
+ const abortControllerRef = useRef(null);
+
+ const fetchCards = async (isLoadMore = false) => {
+ if (isLoadMore && (loadingMoreRef.current || !hasMoreRef.current)) return;
+
+ let signal;
+ try {
+ if (isLoadMore) {
+ setLoadingMore(true);
+ loadingMoreRef.current = true;
+ } else {
+ setLoading(true);
+ setError(null);
+ }
+
+ abortControllerRef.current = new AbortController();
+ ({ signal } = abortControllerRef.current);
+
+ const params = new URLSearchParams({
+ query: searchQuery,
+ game: selectedGame,
+ rarity: selectedRarity,
+ set: selectedSet,
+ minPrice,
+ maxPrice,
+ page: isLoadMore ? pagination.page + 1 : 1,
+ limit: pagination.limit,
+ sort: sortColumn,
+ dir: sortDirection,
+ verifiedOnly: verifiedOnly.toString()
+ });
+
+ const response = await fetch(`/api/admin/cards?${params}`, {
+ signal
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ if (signal.aborted) return;
+
+ if (data.success) {
+ const newCards = data.cards;
+ if (isLoadMore) {
+ setCards(prev => [...prev, ...newCards]);
+ } else {
+ setCards(newCards);
+ }
+ setPagination(data.pagination);
+ setFilters(data.filters);
+ setHasMore(data.pagination.hasMore);
+ hasMoreRef.current = data.pagination.hasMore;
+ } else {
+ throw new Error(data.error || 'Failed to fetch cards');
+ }
+ } catch (err) {
+ if (err.name !== 'AbortError') {
+ console.error('Error fetching admin cards:', err);
+ setError(err.message);
+ }
+ } finally {
+ if (!signal?.aborted) {
+ setLoading(false);
+ setLoadingMore(false);
+ loadingMoreRef.current = false;
+ }
+ }
+ };
+
+ const handleSearchChange = (value) => {
+ setSearchQuery(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleGameChange = (value) => {
+ setSelectedGame(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleRarityChange = (value) => {
+ setSelectedRarity(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleSetChange = (value) => {
+ setSelectedSet(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleMinPriceChange = (value) => {
+ setMinPrice(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleMaxPriceChange = (value) => {
+ setMaxPrice(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleVerifiedChange = (value) => {
+ setVerifiedOnly(value);
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ };
+
+ const handleSort = (column) => {
+ if (sortColumn === column) {
+ setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortColumn(column);
+ setSortDirection('asc');
+ }
+ };
+
+ const handleLoadMore = () => {
+ if (!loadingMore && hasMore) {
+ fetchCards(true);
+ }
+ };
+
+ const handleRefresh = () => {
+ setPagination(prev => ({ ...prev, page: 1 }));
+ setCards([]);
+ fetchCards(false);
+ };
+
+ const handleLimitChange = (limit) => {
+ setPagination(prev => ({ ...prev, limit, page: 1 }));
+ setCards([]);
+ };
+
+ useEffect(() => {
+ fetchCards(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ };
+ }, []);
+
+ return {
+ 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,
+ setSearchQuery,
+ setSelectedGame,
+ setSelectedRarity,
+ setSelectedSet,
+ setMinPrice,
+ setMaxPrice,
+ setVerifiedOnly,
+ setCards,
+ setPagination,
+ setHasMore,
+ setFilters
+ };
+}
\ No newline at end of file
diff --git a/pages/admin/catalog.js b/pages/admin/catalog.js
new file mode 100644
index 0000000..d41a488
--- /dev/null
+++ b/pages/admin/catalog.js
@@ -0,0 +1,20 @@
+import Layout from '../../components/Layout';
+import AdminProtected from '../../components/AdminProtected';
+import AdminCatalogView from '../../components/AdminCatalogView';
+import { useAdminCatalog } from '../../lib/use-admin-catalog.js';
+
+function AdminCatalogPage() {
+ const catalog = useAdminCatalog();
+
+ return (
+
+ {(user) => (
+
+
+
+ )}
+
+ );
+}
+
+export default AdminCatalogPage;
\ No newline at end of file
diff --git a/pages/api/admin/cards.js b/pages/api/admin/cards.js
new file mode 100644
index 0000000..bcfd155
--- /dev/null
+++ b/pages/api/admin/cards.js
@@ -0,0 +1,199 @@
+import { sql } from '../../../lib/sql.js';
+import { withAdmin } from '../../../lib/permission-middleware';
+import { checkImportRateLimit } from '../../../lib/rate-limit.js';
+
+const ALLOWED_SORT_COLUMNS = new Set([
+ 'name', 'set_name', 'set_code', 'card_number', 'rarity', 'game',
+ 'mana_cost', 'cmc', 'card_type', 'current_price', 'market_price',
+ 'prices_updated_at', 'verified', 'created_at', 'updated_at',
+ 'oracle_id', 'illustration_id', 'edhrec_rank', 'released_at'
+]);
+
+const ALLOWED_SORT_DIRS = new Set(['asc', 'desc']);
+
+export default withAdmin(async function handler(req, res, user) {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ const { allowed, reset } = await checkImportRateLimit(req, user.userId);
+ if (!allowed) {
+ res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000));
+ return res.status(429).json({ error: 'Too many attempts. Try again later.' });
+ }
+
+ try {
+ const {
+ query = '',
+ game = 'all',
+ rarity = 'all',
+ set = 'all',
+ minPrice = '',
+ maxPrice = '',
+ page = '1',
+ limit = '50',
+ sort = 'name',
+ dir = 'asc',
+ verifiedOnly = 'false'
+ } = req.query;
+
+ const pageNum = Math.max(1, parseInt(page) || 1);
+ const limitNum = Math.min(200, Math.max(1, parseInt(limit) || 50));
+ const offset = (pageNum - 1) * limitNum;
+ const sortCol = ALLOWED_SORT_COLUMNS.has(sort) ? sort : 'name';
+ const sortDir = ALLOWED_SORT_DIRS.has(dir.toLowerCase()) ? dir.toLowerCase() : 'asc';
+
+ const filters = {
+ hasQuery: query.trim() !== '',
+ hasGame: game !== 'all',
+ hasRarity: rarity !== 'all',
+ hasSet: set !== 'all',
+ hasMinPrice: minPrice && !isNaN(parseFloat(minPrice)),
+ hasMaxPrice: maxPrice && !isNaN(parseFloat(maxPrice)),
+ verifiedOnly: verifiedOnly === 'true'
+ };
+
+ const whereConditions = [];
+ const params = [];
+
+ if (filters.hasQuery) {
+ whereConditions.push(`name ILIKE '%' || $${params.length + 1} || '%'`);
+ params.push(query.trim());
+ }
+ if (filters.hasGame) {
+ whereConditions.push(`game = $${params.length + 1}`);
+ params.push(game);
+ }
+ if (filters.hasRarity) {
+ whereConditions.push(`rarity = $${params.length + 1}`);
+ params.push(rarity);
+ }
+ if (filters.hasSet) {
+ whereConditions.push(`set_name = $${params.length + 1}`);
+ params.push(set);
+ }
+ if (filters.hasMinPrice) {
+ whereConditions.push(`market_price >= $${params.length + 1}`);
+ params.push(parseFloat(minPrice));
+ }
+ if (filters.hasMaxPrice) {
+ whereConditions.push(`market_price <= $${params.length + 1}`);
+ params.push(parseFloat(maxPrice));
+ }
+ if (filters.verifiedOnly) {
+ whereConditions.push(`verified = true`);
+ }
+
+ const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
+
+ const selectColumns = `
+ id, name, set_name, set_code, card_number, rarity, game,
+ mana_cost, cmc, card_type, colors, oracle_text,
+ power, toughness, image_url, stock_image_url,
+ current_price, market_price, prices_updated_at,
+ scryfall_id, verified, oracle_id, illustration_id,
+ color_identity, keywords, legalities, layout,
+ edhrec_rank, reserved, reprint, finishes,
+ released_at, created_at, updated_at
+ `;
+
+ const querySql = `
+ SELECT ${selectColumns}
+ FROM cards
+ ${whereClause}
+ ORDER BY ${sortCol} ${sortDir.toUpperCase()}
+ LIMIT $${params.length + 1} OFFSET $${params.length + 2}
+ `;
+ params.push(limitNum, offset);
+
+ const countSql = `
+ SELECT COUNT(*) as total
+ FROM cards
+ ${whereClause}
+ `;
+
+ const [result, countResult] = await Promise.all([
+ sql.query(querySql, params),
+ sql.query(countSql, params.slice(0, -2))
+ ]);
+
+ const total = parseInt(countResult.rows[0].total);
+ const totalPages = Math.ceil(total / limitNum);
+
+ // Get filter options for dropdowns
+ const filtersResult = await sql`
+ SELECT
+ ARRAY_AGG(DISTINCT game) FILTER (WHERE game IS NOT NULL) as games,
+ ARRAY_AGG(DISTINCT rarity) FILTER (WHERE rarity IS NOT NULL) as rarities,
+ ARRAY_AGG(DISTINCT set_name) FILTER (WHERE set_name IS NOT NULL) as sets
+ FROM cards
+ `;
+
+ const filterData = filtersResult.rows[0];
+
+ const cards = result.rows.map(card => {
+ if (card.colors && typeof card.colors === 'string') {
+ try {
+ card.colors = JSON.parse(card.colors);
+ } catch {
+ card.colors = [];
+ }
+ }
+ if (card.color_identity && typeof card.color_identity === 'string') {
+ try {
+ card.color_identity = JSON.parse(card.color_identity);
+ } catch {
+ card.color_identity = [];
+ }
+ }
+ if (card.keywords && typeof card.keywords === 'string') {
+ try {
+ card.keywords = JSON.parse(card.keywords);
+ } catch {
+ card.keywords = [];
+ }
+ }
+ if (card.legalities && typeof card.legalities === 'string') {
+ try {
+ card.legalities = JSON.parse(card.legalities);
+ } catch {
+ card.legalities = {};
+ }
+ }
+ if (card.finishes && typeof card.finishes === 'string') {
+ try {
+ card.finishes = JSON.parse(card.finishes);
+ } catch {
+ card.finishes = [];
+ }
+ }
+ return card;
+ });
+
+ res.status(200).json({
+ success: true,
+ cards,
+ pagination: {
+ page: pageNum,
+ limit: limitNum,
+ total,
+ pages: totalPages,
+ hasMore: pageNum < totalPages
+ },
+ filters: {
+ games: filterData.games || [],
+ rarities: filterData.rarities || [],
+ sets: filterData.sets || []
+ },
+ sort: { column: sortCol, direction: sortDir }
+ });
+
+ } catch (error) {
+ console.error('Error searching admin cards:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
\ No newline at end of file