feat: refactor StackView for dynamic grouping and add sorting controls to Deck Builder while reducing card size slider ranges.
This commit is contained in:
@@ -3,63 +3,111 @@ import { DraftCard } from '../services/PackGeneratorService';
|
||||
import { FoilOverlay, CardHoverWrapper } from './CardPreview';
|
||||
import { useCardTouch } from '../utils/interaction';
|
||||
|
||||
|
||||
type GroupMode = 'type' | 'color' | 'cmc' | 'rarity';
|
||||
|
||||
interface StackViewProps {
|
||||
cards: DraftCard[];
|
||||
cardWidth?: number;
|
||||
onCardClick?: (card: DraftCard) => void;
|
||||
onHover?: (card: DraftCard | null) => void;
|
||||
disableHoverPreview?: boolean;
|
||||
groupBy?: GroupMode;
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER = [
|
||||
'Creature',
|
||||
'Planeswalker',
|
||||
'Instant',
|
||||
'Sorcery',
|
||||
'Enchantment',
|
||||
'Artifact',
|
||||
'Land',
|
||||
'Battle',
|
||||
'Other'
|
||||
];
|
||||
const GROUPS: Record<GroupMode, string[]> = {
|
||||
type: ['Creature', 'Planeswalker', 'Instant', 'Sorcery', 'Enchantment', 'Artifact', 'Battle', 'Land', 'Other'],
|
||||
color: ['White', 'Blue', 'Black', 'Red', 'Green', 'Multicolor', 'Colorless'],
|
||||
cmc: ['0', '1', '2', '3', '4', '5', '6', '7+'],
|
||||
rarity: ['Mythic', 'Rare', 'Uncommon', 'Common']
|
||||
};
|
||||
|
||||
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false }) => {
|
||||
const getCardGroup = (card: DraftCard, mode: GroupMode): string => {
|
||||
if (mode === 'type') {
|
||||
const typeLine = card.typeLine || '';
|
||||
if (typeLine.includes('Creature')) return 'Creature';
|
||||
if (typeLine.includes('Planeswalker')) return 'Planeswalker';
|
||||
if (typeLine.includes('Instant')) return 'Instant';
|
||||
if (typeLine.includes('Sorcery')) return 'Sorcery';
|
||||
if (typeLine.includes('Enchantment')) return 'Enchantment';
|
||||
if (typeLine.includes('Artifact')) return 'Artifact';
|
||||
if (typeLine.includes('Battle')) return 'Battle';
|
||||
if (typeLine.includes('Land')) return 'Land';
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
if (mode === 'color') {
|
||||
const colors = card.colors || [];
|
||||
if (colors.length > 1) return 'Multicolor';
|
||||
if (colors.length === 0) {
|
||||
// Check if land
|
||||
if ((card.typeLine || '').includes('Land')) return 'Colorless';
|
||||
// Artifacts etc
|
||||
return 'Colorless';
|
||||
}
|
||||
if (colors[0] === 'W') return 'White';
|
||||
if (colors[0] === 'U') return 'Blue';
|
||||
if (colors[0] === 'B') return 'Black';
|
||||
if (colors[0] === 'R') return 'Red';
|
||||
if (colors[0] === 'G') return 'Green';
|
||||
return 'Colorless';
|
||||
}
|
||||
|
||||
if (mode === 'cmc') {
|
||||
const cmc = Math.floor(card.cmc || 0);
|
||||
if (cmc >= 7) return '7+';
|
||||
return cmc.toString();
|
||||
}
|
||||
|
||||
if (mode === 'rarity') {
|
||||
const r = (card.rarity || 'common').toLowerCase();
|
||||
if (r === 'mythic') return 'Mythic';
|
||||
if (r === 'rare') return 'Rare';
|
||||
if (r === 'uncommon') return 'Uncommon';
|
||||
return 'Common';
|
||||
}
|
||||
|
||||
return 'Other';
|
||||
};
|
||||
|
||||
|
||||
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color' }) => {
|
||||
|
||||
const categorizedCards = useMemo(() => {
|
||||
const categories: Record<string, DraftCard[]> = {};
|
||||
CATEGORY_ORDER.forEach(c => categories[c] = []);
|
||||
const groupKeys = GROUPS[groupBy];
|
||||
groupKeys.forEach(k => categories[k] = []);
|
||||
|
||||
cards.forEach(card => {
|
||||
let category = 'Other';
|
||||
const typeLine = card.typeLine || '';
|
||||
|
||||
if (typeLine.includes('Creature')) category = 'Creature'; // Includes Artifact Creature, Ench Creature
|
||||
else if (typeLine.includes('Planeswalker')) category = 'Planeswalker';
|
||||
else if (typeLine.includes('Instant')) category = 'Instant';
|
||||
else if (typeLine.includes('Sorcery')) category = 'Sorcery';
|
||||
else if (typeLine.includes('Enchantment')) category = 'Enchantment';
|
||||
else if (typeLine.includes('Artifact')) category = 'Artifact';
|
||||
else if (typeLine.includes('Battle')) category = 'Battle';
|
||||
else if (typeLine.includes('Land')) category = 'Land';
|
||||
|
||||
// Special handling: Commander? usually Creature or Planeswalker
|
||||
// Ensure it lands in one of the predefined bins
|
||||
|
||||
categories[category].push(card);
|
||||
const group = getCardGroup(card, groupBy);
|
||||
if (categories[group]) {
|
||||
categories[group].push(card);
|
||||
} else {
|
||||
// Fallback for unexpected (shouldn't happen with defined logic coverage)
|
||||
if (!categories['Other']) categories['Other'] = [];
|
||||
categories['Other'].push(card);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort cards within categories by CMC (low to high)? Or Rarity?
|
||||
// Archidekt usually sorts by CMC.
|
||||
// Sort cards within categories by CMC (low to high)
|
||||
// Secondary sort by Name
|
||||
Object.keys(categories).forEach(key => {
|
||||
categories[key].sort((a, b) => (a.cmc || 0) - (b.cmc || 0));
|
||||
categories[key].sort((a, b) => {
|
||||
const cmcA = a.cmc || 0;
|
||||
const cmcB = b.cmc || 0;
|
||||
if (cmcA !== cmcB) return cmcA - cmcB;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
|
||||
return categories;
|
||||
}, [cards]);
|
||||
}, [cards, groupBy]);
|
||||
|
||||
const activeGroups = GROUPS[groupBy];
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 overflow-x-auto pb-8 snap-x items-start">
|
||||
{CATEGORY_ORDER.map(category => {
|
||||
{activeGroups.map(category => {
|
||||
const catCards = categorizedCards[category];
|
||||
if (catCards.length === 0) return null;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
|
||||
const [cardWidth, setCardWidth] = useState(() => {
|
||||
const saved = localStorage.getItem('cube_cardWidth');
|
||||
return saved ? parseInt(saved) : 100;
|
||||
return saved ? parseInt(saved) : 60;
|
||||
});
|
||||
|
||||
// --- Persistence Effects ---
|
||||
@@ -838,8 +838,8 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
<div className="w-3 h-4 rounded border border-slate-500 bg-slate-700" title="Small Cards" />
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="300"
|
||||
min="60"
|
||||
max="200"
|
||||
step="1"
|
||||
value={cardWidth}
|
||||
onChange={(e) => setCardWidth(parseInt(e.target.value))}
|
||||
|
||||
@@ -134,7 +134,8 @@ const CardsDisplay: React.FC<{
|
||||
onHover: (c: any) => void;
|
||||
emptyMessage: string;
|
||||
source: 'pool' | 'deck';
|
||||
}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage, source }) => {
|
||||
groupBy?: 'type' | 'color' | 'cmc' | 'rarity';
|
||||
}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage, source, groupBy = 'color' }) => {
|
||||
if (cards.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-500 opacity-50 p-8 border-2 border-dashed border-slate-700/50 rounded-lg">
|
||||
@@ -174,6 +175,7 @@ const CardsDisplay: React.FC<{
|
||||
}}
|
||||
onHover={(c) => onHover(c)}
|
||||
disableHoverPreview={true}
|
||||
groupBy={groupBy}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -213,8 +215,9 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
// Unlimited Timer (Static for now)
|
||||
const [timer] = useState<string>("Unlimited");
|
||||
const [layout, setLayout] = useState<'vertical' | 'horizontal'>('vertical');
|
||||
const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('grid');
|
||||
const [cardWidth, setCardWidth] = useState(100);
|
||||
const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('stack'); // Default to stack as requested? Or keep grid. User didn't say default view, just default Order.
|
||||
const [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>('color');
|
||||
const [cardWidth, setCardWidth] = useState(60);
|
||||
|
||||
const [pool, setPool] = useState<any[]>(initialPool);
|
||||
const [deck, setDeck] = useState<any[]>([]);
|
||||
@@ -459,15 +462,10 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
return (
|
||||
<div className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col select-none" onContextMenu={(e) => e.preventDefault()}>
|
||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
{/* Global Toolbar */}
|
||||
{/* Global Toolbar */}
|
||||
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-4 shrink-0 overflow-x-auto text-xs sm:text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Layout Switcher */}
|
||||
<div className="hidden sm:flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setLayout('vertical')} className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Vertical Split"><Columns className="w-4 h-4" /></button>
|
||||
<button onClick={() => setLayout('horizontal')} className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Horizontal Split"><LayoutTemplate className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setViewMode('list')} className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="List View"><List className="w-4 h-4" /></button>
|
||||
@@ -475,13 +473,36 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
<button onClick={() => setViewMode('stack')} className={`p-1.5 rounded ${viewMode === 'stack' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Stack View"><Layers className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* Group By Dropdown (Only relevant for Stack View usually, but nice to have) */}
|
||||
{viewMode === 'stack' && (
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700 h-9 items-center px-2 gap-2">
|
||||
<span className="text-[10px] text-slate-500 uppercase font-bold">Sort:</span>
|
||||
<select
|
||||
value={groupBy}
|
||||
onChange={(e) => setGroupBy(e.target.value as any)}
|
||||
className="bg-transparent text-xs font-bold text-white outline-none cursor-pointer"
|
||||
>
|
||||
<option value="color">Color</option>
|
||||
<option value="type">Type</option>
|
||||
<option value="cmc">Mana Value</option>
|
||||
<option value="rarity">Rarity</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Layout Switcher */}
|
||||
<div className="hidden sm:flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setLayout('vertical')} className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Vertical Split"><Columns className="w-4 h-4" /></button>
|
||||
<button onClick={() => setLayout('horizontal')} className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Horizontal Split"><LayoutTemplate className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<div className="hidden sm:flex items-center gap-2 bg-slate-900 rounded-lg px-2 py-1 border border-slate-700 h-9">
|
||||
<div className="w-2 h-3 rounded border border-slate-500 bg-slate-700" />
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="300"
|
||||
min="60"
|
||||
max="200"
|
||||
step="1"
|
||||
value={cardWidth}
|
||||
onChange={(e) => setCardWidth(parseInt(e.target.value))}
|
||||
@@ -570,7 +591,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" />
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
{/* Deck Column */}
|
||||
@@ -579,7 +600,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
<span>Library ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" />
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
</div>
|
||||
@@ -592,7 +613,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" />
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
{/* Bottom: Deck */}
|
||||
@@ -601,7 +622,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
<span>Library ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" />
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
||||
|
||||
const [cardScale, setCardScale] = useState<number>(() => {
|
||||
const saved = localStorage.getItem('draft_cardScale');
|
||||
return saved ? parseFloat(saved) : 0.5;
|
||||
return saved ? parseFloat(saved) : 0.35;
|
||||
});
|
||||
|
||||
const [layout, setLayout] = useState<'vertical' | 'horizontal'>('horizontal');
|
||||
@@ -190,8 +190,8 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
||||
<label className="text-[10px] text-slate-500 uppercase font-bold tracking-wider">Card Size</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
min="0.35"
|
||||
max="1.0"
|
||||
step="0.01"
|
||||
value={cardScale}
|
||||
onChange={(e) => setCardScale(parseFloat(e.target.value))}
|
||||
|
||||
Reference in New Issue
Block a user