feat: implement customizable vertical and horizontal deck builder layouts with a new layout switcher and associated rendering refactors.
This commit is contained in:
@@ -84,3 +84,4 @@
|
|||||||
- [Gameplay Magnified View & Timeout](./devlog/2025-12-17-161500_gameplay_magnified_view_and_timeout.md): Completed. Added magnified view with full card details (Oracle text, type, mana) to gameplay and disabled timeout.
|
- [Gameplay Magnified View & Timeout](./devlog/2025-12-17-161500_gameplay_magnified_view_and_timeout.md): Completed. Added magnified view with full card details (Oracle text, type, mana) to gameplay and disabled timeout.
|
||||||
- [Test Deck Feature](./devlog/2025-12-17-162500_test_deck_feature.md): Completed. Implemented "Test Solo" button in Cube Manager to instantly start a solo game with a randomized deck from generated packs.
|
- [Test Deck Feature](./devlog/2025-12-17-162500_test_deck_feature.md): Completed. Implemented "Test Solo" button in Cube Manager to instantly start a solo game with a randomized deck from generated packs.
|
||||||
- [Update Deck Auto-Fill](./devlog/2025-12-17-165500_update_deck_autofill.md): Completed. Updated deck builder "Auto-Fill" to add lands as individual cards to the deck list for easier management.
|
- [Update Deck Auto-Fill](./devlog/2025-12-17-165500_update_deck_autofill.md): Completed. Updated deck builder "Auto-Fill" to add lands as individual cards to the deck list for easier management.
|
||||||
|
- [Customizable Deck Builder Layout](./devlog/2025-12-17-170000_customizable_deck_builder.md): Completed. Implemented switchable Vertical (Side-by-Side) and Horizontal (Top-Bottom) layouts, with an integrated, improved Land Station.
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Customizable Deck Builder Layout
|
||||||
|
|
||||||
|
## Request
|
||||||
|
The user wants to customize the Deck Builder interface with the following features:
|
||||||
|
1. **Layout Modes**:
|
||||||
|
* **Vertical View (Default)**: The current 3-column layout ([Zoom] | [Pool] | [Deck + Lands]).
|
||||||
|
* **Horizontal View**: A new layout where Pool is above the Deck. Land Station should be to the left of the Card Pool in this mode.
|
||||||
|
2. **Land Station Updates**:
|
||||||
|
* Remove "(Unlimited)" text.
|
||||||
|
* Increase container height.
|
||||||
|
* Integrate proper Land Advisor into the Land Station container to save space.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
### New Layout State
|
||||||
|
- State: `layout: 'vertical' | 'horizontal'`
|
||||||
|
- Toggle: A button group or switch to change layouts.
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
I will extract the core sections into render functions or variables to move them around easily.
|
||||||
|
- `renderZoomSidebar()`
|
||||||
|
- `renderPool()`
|
||||||
|
- `renderDeck()`
|
||||||
|
- `renderLandStation()` (This will now include the Land Advisor inside it)
|
||||||
|
|
||||||
|
### Horizontal Layout Grid
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
[Zoom Sidebar (Fixed Left)] | [Main Content (Flex Column)]
|
||||||
|
|
|
||||||
|
|-- [Top Row (Flex Row)]
|
||||||
|
| |-- [Land Station (width fixed or flex)]
|
||||||
|
| |-- [Pool (Flex 1)]
|
||||||
|
|
|
||||||
|
|-- [Bottom Row (Flex 1)]
|
||||||
|
|-- [Deck]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vertical Layout Grid (Current)
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
[Zoom Sidebar] | [Pool] | [Deck + Land Station]
|
||||||
|
```
|
||||||
|
*Note: In current layout, Land Station is stacked vertically with Deck in the 3rd column. User is fine with "exactly how is it now" for vertical.*
|
||||||
|
|
||||||
|
### Land Station Refactoring
|
||||||
|
- Combine `Advice Panel` and `Land Station` div.
|
||||||
|
- Remove `(Unlimited)`.
|
||||||
|
- Increase height (e.g., `h-64` or `min-h-[200px]`).
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
1. Read `DeckBuilderView.tsx` (already read).
|
||||||
|
2. Refactor to extract render helper functions for clear modularity.
|
||||||
|
3. Add `layout` state and toggle UI.
|
||||||
|
4. Implement CSS grids/flex layouts for both modes.
|
||||||
|
5. Modify Land Station to include Advisor and styling updates.
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { socketService } from '../../services/SocketService';
|
import { socketService } from '../../services/SocketService';
|
||||||
import { Save, Layers, Clock } from 'lucide-react';
|
import { Save, Layers, Clock, Columns, LayoutTemplate } from 'lucide-react';
|
||||||
|
|
||||||
interface DeckBuilderViewProps {
|
interface DeckBuilderViewProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -13,21 +12,12 @@ interface DeckBuilderViewProps {
|
|||||||
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
||||||
// Unlimited Timer (Static for now)
|
// Unlimited Timer (Static for now)
|
||||||
const [timer] = useState<string>("Unlimited");
|
const [timer] = useState<string>("Unlimited");
|
||||||
|
const [layout, setLayout] = useState<'vertical' | 'horizontal'>('vertical');
|
||||||
const [pool, setPool] = useState<any[]>(initialPool);
|
const [pool, setPool] = useState<any[]>(initialPool);
|
||||||
const [deck, setDeck] = useState<any[]>([]);
|
const [deck, setDeck] = useState<any[]>([]);
|
||||||
const [lands, setLands] = useState({ Plains: 0, Island: 0, Swamp: 0, Mountain: 0, Forest: 0 });
|
const [lands, setLands] = useState({ Plains: 0, Island: 0, Swamp: 0, Mountain: 0, Forest: 0 });
|
||||||
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
||||||
|
|
||||||
/*
|
|
||||||
// Disable timer countdown
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setTimer(t => t > 0 ? t - 1 : 0);
|
|
||||||
}, 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- Land Advice Logic ---
|
// --- Land Advice Logic ---
|
||||||
const landSuggestion = React.useMemo(() => {
|
const landSuggestion = React.useMemo(() => {
|
||||||
const targetLands = 17;
|
const targetLands = 17;
|
||||||
@@ -56,8 +46,6 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
|
|
||||||
totalPips = Object.values(pips).reduce((a, b) => a + b, 0);
|
totalPips = Object.values(pips).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
// If no colored pips (artifacts only?), suggest even split or just return 0s?
|
|
||||||
// Let's assume proportional to 1 if 0 to avoid div by zero.
|
|
||||||
if (totalPips === 0) return null;
|
if (totalPips === 0) return null;
|
||||||
|
|
||||||
// Distribute
|
// Distribute
|
||||||
@@ -181,9 +169,190 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
return [...(availableBasicLands || [])].sort((a, b) => a.name.localeCompare(b.name));
|
return [...(availableBasicLands || [])].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, [availableBasicLands]);
|
}, [availableBasicLands]);
|
||||||
|
|
||||||
|
// --- Sub Actions ---
|
||||||
|
const renderAdvisorContent = () => {
|
||||||
|
if (!landSuggestion) return <span className="text-xs text-slate-500 italic">Add colored spells to get advice.</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(Object.entries(landSuggestion) as [string, number][]).map(([type, count]) => {
|
||||||
|
if (count === 0) return null;
|
||||||
|
let colorClass = "text-slate-300";
|
||||||
|
if (type === 'Plains') colorClass = "text-amber-200";
|
||||||
|
if (type === 'Island') colorClass = "text-blue-200";
|
||||||
|
if (type === 'Swamp') colorClass = "text-purple-200";
|
||||||
|
if (type === 'Mountain') colorClass = "text-red-200";
|
||||||
|
if (type === 'Forest') colorClass = "text-emerald-200";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type} className={`font-bold ${colorClass} text-xs flex items-center gap-1`}>
|
||||||
|
<span>{type.substring(0, 1)}:</span>
|
||||||
|
<span>{count}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={applySuggestion}
|
||||||
|
className="bg-emerald-700 hover:bg-emerald-600 text-white text-[10px] px-2 py-1 rounded shadow transition-colors font-bold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
Auto-Fill
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Sections ---
|
||||||
|
const renderLandStation = () => (
|
||||||
|
<div className={`bg-slate-800 rounded-lg border border-slate-700 flex flex-col ${layout === 'horizontal' ? 'h-full' : 'h-72'} transition-all`}>
|
||||||
|
<div className="p-3 border-b border-slate-700 flex flex-col gap-2 shrink-0 bg-slate-900/30">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-sm font-bold text-slate-400 uppercase">Land Station</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Integrated Advisor */}
|
||||||
|
<div className="bg-slate-950/50 rounded border border-white/5 p-2 flex flex-col gap-1">
|
||||||
|
<span className="text-[10px] text-emerald-400 font-bold uppercase flex items-center gap-1">
|
||||||
|
<Layers className="w-3 h-3" /> Land Advisor (Target: 17)
|
||||||
|
</span>
|
||||||
|
{renderAdvisorContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 bg-slate-900/50 rounded-b-lg">
|
||||||
|
{availableBasicLands && availableBasicLands.length > 0 ? (
|
||||||
|
<div className={`grid ${layout === 'horizontal' ? 'grid-cols-2' : 'grid-flow-col auto-cols-max'} gap-2 content-start`}>
|
||||||
|
{/* Note: horizontal layout gets grid-cols-2 for vertical scrolling list feeling, vertical layout gets side-scrolling or wrapped */}
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center">
|
||||||
|
{sortedLands.map((land) => (
|
||||||
|
<div
|
||||||
|
key={land.scryfallId}
|
||||||
|
className="relative group cursor-pointer"
|
||||||
|
onClick={() => addLandToDeck(land)}
|
||||||
|
onMouseEnter={() => setHoveredCard(land)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={land.image || land.image_uris?.normal}
|
||||||
|
className="w-20 hover:scale-105 transition-transform rounded shadow-lg"
|
||||||
|
alt={land.name}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 bg-black/40 rounded transition-opacity">
|
||||||
|
<span className="text-white font-bold text-xs bg-black/50 px-1 rounded">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Fallback counter UI
|
||||||
|
<div className="flex flex-col gap-2 p-2">
|
||||||
|
{Object.keys(lands).map(type => (
|
||||||
|
<div key={type} className="flex items-center justify-between bg-slate-800 p-2 rounded">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center font-bold text-xs border
|
||||||
|
${type === 'Plains' ? 'bg-amber-900/50 border-amber-500 text-amber-200' : ''}
|
||||||
|
${type === 'Island' ? 'bg-blue-900/50 border-blue-500 text-blue-200' : ''}
|
||||||
|
${type === 'Swamp' ? 'bg-purple-900/50 border-purple-500 text-purple-200' : ''}
|
||||||
|
${type === 'Mountain' ? 'bg-red-900/50 border-red-500 text-red-200' : ''}
|
||||||
|
${type === 'Forest' ? 'bg-green-900/50 border-green-500 text-green-200' : ''}
|
||||||
|
`}>
|
||||||
|
{type[0]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={() => handleLandChange(type, -1)} className="w-6 h-6 bg-slate-700 hover:bg-slate-600 rounded text-slate-300 font-bold">-</button>
|
||||||
|
<span className="w-6 text-center text-sm font-bold">{lands[type as keyof typeof lands]}</span>
|
||||||
|
<button onClick={() => handleLandChange(type, 1)} className="w-6 h-6 bg-slate-700 hover:bg-slate-600 rounded text-slate-300 font-bold">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPool = () => (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-center mb-4 shrink-0">
|
||||||
|
<h2 className="text-xl font-bold flex items-center gap-2"><Layers /> Card Pool ({pool.length})</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 bg-slate-950/50 rounded-lg custom-scrollbar">
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center content-start">
|
||||||
|
{pool.map((card) => (
|
||||||
|
<img
|
||||||
|
key={card.id}
|
||||||
|
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
||||||
|
className="w-28 hover:scale-110 transition-transform cursor-pointer rounded shadow-md"
|
||||||
|
onClick={() => addToDeck(card)}
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
title={card.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDeck = () => (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-center mb-4 shrink-0">
|
||||||
|
<h2 className="text-xl font-bold">Your Deck ({deck.length + Object.values(lands).reduce((a, b) => a + b, 0)})</h2>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2 text-amber-400 font-mono text-xl font-bold bg-slate-800 px-3 py-1 rounded border border-amber-500/30">
|
||||||
|
<Clock className="w-5 h-5" /> {formatTime(timer)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={submitDeck}
|
||||||
|
className="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" /> Submit Deck
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 bg-slate-950/50 rounded-lg custom-scrollbar">
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center content-start">
|
||||||
|
{deck.map((card) => (
|
||||||
|
<img
|
||||||
|
key={card.id}
|
||||||
|
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
||||||
|
className="w-28 hover:scale-110 transition-transform cursor-pointer rounded shadow-md"
|
||||||
|
onClick={() => removeFromDeck(card)}
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
title={card.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 w-full flex h-full bg-slate-900 text-white">
|
<div className="flex-1 w-full flex h-full bg-slate-900 text-white overflow-hidden relative">
|
||||||
{/* Column 1: Zoom Sidebar */}
|
{/* View Switcher - Absolute Positioned */}
|
||||||
|
<div className="absolute bottom-4 left-84 z-20 flex bg-slate-800/80 backdrop-blur rounded-lg p-1 border border-slate-700 shadow-xl gap-1" style={{ left: '330px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setLayout('vertical')}
|
||||||
|
className={`p-2 rounded flex items-center gap-2 text-xs font-bold transition-colors ${layout === 'vertical' ? 'bg-slate-600 text-white shadow' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
title="Cards Side-by-Side"
|
||||||
|
>
|
||||||
|
<Columns className="w-4 h-4" /> Vertical
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLayout('horizontal')}
|
||||||
|
className={`p-2 rounded flex items-center gap-2 text-xs font-bold transition-colors ${layout === 'horizontal' ? 'bg-slate-600 text-white shadow' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
title="Pool Above Deck"
|
||||||
|
>
|
||||||
|
<LayoutTemplate className="w-4 h-4" /> Horizontal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 1: Zoom Sidebar (Always visible) */}
|
||||||
<div className="hidden xl:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800 bg-slate-950/50 z-10 p-4">
|
<div className="hidden xl:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800 bg-slate-950/50 z-10 p-4">
|
||||||
{hoveredCard ? (
|
{hoveredCard ? (
|
||||||
<div className="animate-in fade-in slide-in-from-left-4 duration-200 sticky top-4 w-full">
|
<div className="animate-in fade-in slide-in-from-left-4 duration-200 sticky top-4 w-full">
|
||||||
@@ -212,156 +381,41 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column 2: Pool */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 p-4 flex flex-col border-r border-slate-700 min-w-0">
|
{layout === 'vertical' ? (
|
||||||
<div className="flex justify-between items-center mb-4">
|
<>
|
||||||
<h2 className="text-xl font-bold flex items-center gap-2"><Layers /> Card Pool ({pool.length})</h2>
|
{/* Vertical: Column 2 (Pool) */}
|
||||||
</div>
|
<div className="flex-1 p-4 flex flex-col border-r border-slate-700 min-w-0">
|
||||||
<div className="flex-1 overflow-y-auto p-2 bg-slate-950/50 rounded-lg custom-scrollbar">
|
{renderPool()}
|
||||||
<div className="flex flex-wrap gap-2 justify-center content-start">
|
|
||||||
{pool.map((card) => (
|
|
||||||
<img
|
|
||||||
key={card.id}
|
|
||||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
|
||||||
className="w-28 hover:scale-110 transition-transform cursor-pointer rounded shadow-md"
|
|
||||||
onClick={() => addToDeck(card)}
|
|
||||||
onMouseEnter={() => setHoveredCard(card)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
title={card.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/* Vertical: Column 3 (Deck & Lands) */}
|
||||||
</div>
|
<div className="flex-1 p-4 flex flex-col min-w-0">
|
||||||
|
{renderDeck()}
|
||||||
{/* Column 3: Deck & Lands */}
|
<div className="mt-4">
|
||||||
<div className="flex-1 p-4 flex flex-col min-w-0">
|
{renderLandStation()}
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-xl font-bold">Your Deck ({deck.length + Object.values(lands).reduce((a, b) => a + b, 0)})</h2>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2 text-amber-400 font-mono text-xl font-bold bg-slate-800 px-3 py-1 rounded border border-amber-500/30">
|
|
||||||
<Clock className="w-5 h-5" /> {formatTime(timer)}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={submitDeck}
|
|
||||||
className="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105"
|
|
||||||
>
|
|
||||||
<Save className="w-4 h-4" /> Submit Deck
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
{/* Deck View */}
|
/* Horizontal Layout */
|
||||||
<div className="flex-1 overflow-y-auto p-2 bg-slate-950/50 rounded-lg mb-4 custom-scrollbar">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
<div className="flex flex-wrap gap-2 justify-center content-start">
|
{/* Top Row: Lands + Pool */}
|
||||||
{deck.map((card) => (
|
<div className="flex-1 flex min-h-0 border-b border-slate-700">
|
||||||
<img
|
{/* Land Station (Left of Pool) */}
|
||||||
key={card.id}
|
<div className="w-[300px] p-4 border-r border-slate-700 flex flex-col">
|
||||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
{renderLandStation()}
|
||||||
className="w-28 hover:scale-110 transition-transform cursor-pointer rounded shadow-md"
|
</div>
|
||||||
onClick={() => removeFromDeck(card)}
|
{/* Pool */}
|
||||||
onMouseEnter={() => setHoveredCard(card)}
|
<div className="flex-1 p-4 flex flex-col min-w-0">
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
{renderPool()}
|
||||||
title={card.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
|
|
||||||
{/* Advice Panel */}
|
|
||||||
<div className="bg-slate-800 rounded-lg p-3 border border-slate-700 flex justify-between items-center">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-xs text-slate-400 font-bold uppercase flex items-center gap-2">
|
|
||||||
<Layers className="w-3 h-3 text-emerald-400" /> Land Advisor (Target: 17)
|
|
||||||
</span>
|
|
||||||
<div className="text-xs text-slate-500 mt-1">
|
|
||||||
Based on your deck's mana symbols.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{landSuggestion ? (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{(Object.entries(landSuggestion) as [string, number][]).map(([type, count]) => {
|
|
||||||
if (count === 0) return null;
|
|
||||||
let colorClass = "text-slate-300";
|
|
||||||
if (type === 'Plains') colorClass = "text-amber-200";
|
|
||||||
if (type === 'Island') colorClass = "text-blue-200";
|
|
||||||
if (type === 'Swamp') colorClass = "text-purple-200";
|
|
||||||
if (type === 'Mountain') colorClass = "text-red-200";
|
|
||||||
if (type === 'Forest') colorClass = "text-emerald-200";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={type} className={`font-bold ${colorClass} text-sm flex items-center gap-1`}>
|
|
||||||
<span>{type.substring(0, 1)}:</span>
|
|
||||||
<span>{count}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={applySuggestion}
|
|
||||||
className="bg-emerald-700 hover:bg-emerald-600 text-white text-xs px-3 py-1 rounded shadow transition-colors font-bold"
|
|
||||||
>
|
|
||||||
Auto-Fill
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-slate-500 italic">Add colored spells to get advice.</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* Bottom Row: Deck */}
|
||||||
{/* Land Station */}
|
<div className="h-[40%] p-4 flex flex-col bg-slate-900/50">
|
||||||
<div className="h-48 bg-slate-800 rounded-lg p-4 border border-slate-700 flex flex-col">
|
{renderDeck()}
|
||||||
<h3 className="text-sm font-bold text-slate-400 uppercase mb-2">Land Station (Unlimited)</h3>
|
|
||||||
|
|
||||||
{availableBasicLands && availableBasicLands.length > 0 ? (
|
|
||||||
<div className="flex-1 overflow-x-auto flex items-center gap-3 custom-scrollbar p-2 bg-slate-900/50 rounded-lg">
|
|
||||||
{sortedLands.map((land) => (
|
|
||||||
<div
|
|
||||||
key={land.scryfallId}
|
|
||||||
className="flex-shrink-0 relative group cursor-pointer"
|
|
||||||
onClick={() => addLandToDeck(land)}
|
|
||||||
onMouseEnter={() => setHoveredCard(land)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={land.image || land.image_uris?.normal}
|
|
||||||
className="w-24 rounded shadow-lg group-hover:scale-110 transition-transform"
|
|
||||||
alt={land.name}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 bg-black/40 rounded transition-opacity">
|
|
||||||
<span className="text-white font-bold text-xs bg-black/50 px-2 py-1 rounded">+ Add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-around items-center h-full">
|
|
||||||
{Object.keys(lands).map(type => (
|
|
||||||
<div key={type} className="flex flex-col items-center gap-1">
|
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-xs border-2
|
|
||||||
${type === 'Plains' ? 'bg-amber-100 border-amber-300 text-amber-900' : ''}
|
|
||||||
${type === 'Island' ? 'bg-blue-100 border-blue-300 text-blue-900' : ''}
|
|
||||||
${type === 'Swamp' ? 'bg-purple-100 border-purple-300 text-purple-900' : ''}
|
|
||||||
${type === 'Mountain' ? 'bg-red-100 border-red-300 text-red-900' : ''}
|
|
||||||
${type === 'Forest' ? 'bg-green-100 border-green-300 text-green-900' : ''}
|
|
||||||
`}>
|
|
||||||
{type[0]}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button onClick={() => handleLandChange(type, -1)} className="w-8 h-8 bg-slate-700 rounded hover:bg-slate-600 flex items-center justify-center text-lg font-bold text-slate-300">-</button>
|
|
||||||
<span className="w-8 text-center font-bold text-lg">{lands[type as keyof typeof lands]}</span>
|
|
||||||
<button onClick={() => handleLandChange(type, 1)} className="w-8 h-8 bg-slate-700 rounded hover:bg-slate-600 flex items-center justify-center text-lg font-bold text-slate-300">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user