feat: Persist DeckBuilder UI settings and library height to local storage, and fix sort dropdown positioning.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { socketService } from '../../services/SocketService';
|
import { socketService } from '../../services/SocketService';
|
||||||
import { Save, Layers, Clock, Columns, LayoutTemplate, List, LayoutGrid, ChevronDown, Check, GripVertical } from 'lucide-react';
|
import { Save, Layers, Clock, Columns, LayoutTemplate, List, LayoutGrid, ChevronDown, Check } from 'lucide-react';
|
||||||
import { StackView } from '../../components/StackView';
|
import { StackView } from '../../components/StackView';
|
||||||
import { FoilOverlay } from '../../components/CardPreview';
|
import { FoilOverlay } from '../../components/CardPreview';
|
||||||
import { DraftCard } from '../../services/PackGeneratorService';
|
import { DraftCard } from '../../services/PackGeneratorService';
|
||||||
@@ -220,10 +220,22 @@ const CardsDisplay: React.FC<{
|
|||||||
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 [layout, setLayout] = useState<'vertical' | 'horizontal'>(() => {
|
||||||
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 saved = typeof window !== 'undefined' ? localStorage.getItem('deck_layout') : null;
|
||||||
const [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>('color');
|
return (saved as 'vertical' | 'horizontal') || 'vertical';
|
||||||
const [cardWidth, setCardWidth] = useState(60);
|
});
|
||||||
|
const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>(() => {
|
||||||
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_viewMode') : null;
|
||||||
|
return (saved as 'list' | 'grid' | 'stack') || 'stack';
|
||||||
|
});
|
||||||
|
const [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>(() => {
|
||||||
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_groupBy') : null;
|
||||||
|
return (saved as 'type' | 'color' | 'cmc' | 'rarity') || 'color';
|
||||||
|
});
|
||||||
|
const [cardWidth, setCardWidth] = useState(() => {
|
||||||
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_cardWidth') : null;
|
||||||
|
return saved ? parseInt(saved, 10) : 60;
|
||||||
|
});
|
||||||
// Local state for smooth slider
|
// Local state for smooth slider
|
||||||
const [localCardWidth, setLocalCardWidth] = useState(cardWidth);
|
const [localCardWidth, setLocalCardWidth] = useState(cardWidth);
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
@@ -240,28 +252,29 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
|
|
||||||
// --- Resize State ---
|
// --- Resize State ---
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||||
const saved = localStorage.getItem('deck_sidebarWidth');
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_sidebarWidth') : null;
|
||||||
return saved ? parseInt(saved, 10) : 320;
|
return saved ? parseInt(saved, 10) : 320;
|
||||||
});
|
});
|
||||||
const [poolHeightPercent, setPoolHeightPercent] = useState(() => {
|
// We now control the Library (Bottom) height in pixels, matching DraftView consistency
|
||||||
const saved = localStorage.getItem('deck_poolHeightPercent');
|
const [libraryHeight, setLibraryHeight] = useState(() => {
|
||||||
return saved ? parseFloat(saved) : 60;
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_libraryHeight') : null;
|
||||||
|
return saved ? parseInt(saved, 10) : 300;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
||||||
const poolRef = React.useRef<HTMLDivElement>(null);
|
const libraryRef = React.useRef<HTMLDivElement>(null);
|
||||||
const resizingState = React.useRef<{
|
const resizingState = React.useRef<{
|
||||||
startX: number,
|
startX: number,
|
||||||
startY: number,
|
startY: number,
|
||||||
startWidth: number,
|
startWidth: number,
|
||||||
startHeightPercent: number,
|
startHeight: number,
|
||||||
active: 'sidebar' | 'pool' | null
|
active: 'sidebar' | 'library' | null
|
||||||
}>({ startX: 0, startY: 0, startWidth: 0, startHeightPercent: 0, active: null });
|
}>({ startX: 0, startY: 0, startWidth: 0, startHeight: 0, active: null });
|
||||||
|
|
||||||
// Initial visual set
|
// Initial visual set
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (sidebarRef.current) sidebarRef.current.style.width = `${sidebarWidth}px`;
|
if (sidebarRef.current) sidebarRef.current.style.width = `${sidebarWidth}px`;
|
||||||
if (poolRef.current) poolRef.current.style.height = `${poolHeightPercent}%`;
|
if (libraryRef.current) libraryRef.current.style.height = `${libraryHeight}px`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Persist Resize
|
// Persist Resize
|
||||||
@@ -270,8 +283,14 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
}, [sidebarWidth]);
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('deck_poolHeightPercent', poolHeightPercent.toString());
|
localStorage.setItem('deck_libraryHeight', libraryHeight.toString());
|
||||||
}, [poolHeightPercent]);
|
}, [libraryHeight]);
|
||||||
|
|
||||||
|
// Persist Settings
|
||||||
|
useEffect(() => localStorage.setItem('deck_layout', layout), [layout]);
|
||||||
|
useEffect(() => localStorage.setItem('deck_viewMode', viewMode), [viewMode]);
|
||||||
|
useEffect(() => localStorage.setItem('deck_groupBy', groupBy), [groupBy]);
|
||||||
|
useEffect(() => localStorage.setItem('deck_cardWidth', cardWidth.toString()), [cardWidth]);
|
||||||
|
|
||||||
const [pool, setPool] = useState<any[]>(initialPool);
|
const [pool, setPool] = useState<any[]>(initialPool);
|
||||||
const [deck, setDeck] = useState<any[]>([]);
|
const [deck, setDeck] = useState<any[]>([]);
|
||||||
@@ -451,22 +470,18 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
|
|
||||||
// --- Resize Handlers ---
|
// --- Resize Handlers ---
|
||||||
// --- Resize Handlers ---
|
// --- Resize Handlers ---
|
||||||
const handleResizeStart = (type: 'sidebar' | 'pool', e: React.MouseEvent | React.TouchEvent) => {
|
const handleResizeStart = (type: 'sidebar' | 'library', e: React.MouseEvent | React.TouchEvent) => {
|
||||||
// Prevent default to avoid scrolling/selection
|
// Prevent default to avoid scrolling/selection
|
||||||
if (e.cancelable) e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
|
|
||||||
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
||||||
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
const containerTop = 56;
|
|
||||||
const containerHeight = window.innerHeight - containerTop;
|
|
||||||
const currentPoolHeight = poolRef.current?.getBoundingClientRect().height || (containerHeight * 0.6);
|
|
||||||
|
|
||||||
resizingState.current = {
|
resizingState.current = {
|
||||||
startX: clientX,
|
startX: clientX,
|
||||||
startY: clientY,
|
startY: clientY,
|
||||||
startWidth: sidebarRef.current?.getBoundingClientRect().width || 320,
|
startWidth: sidebarRef.current?.getBoundingClientRect().width || 320,
|
||||||
startHeightPercent: poolHeightPercent,
|
startHeight: libraryRef.current?.getBoundingClientRect().height || 300,
|
||||||
active: type
|
active: type
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -492,13 +507,11 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
sidebarRef.current.style.width = `${newWidth}px`;
|
sidebarRef.current.style.width = `${newWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resizingState.current.active === 'pool' && poolRef.current) {
|
if (resizingState.current.active === 'library' && libraryRef.current) {
|
||||||
const containerTop = 56;
|
// Dragging UP increases height of bottom panel
|
||||||
const containerHeight = window.innerHeight - containerTop;
|
const delta = resizingState.current.startY - clientY;
|
||||||
const relativeY = clientY - containerTop;
|
const newHeight = Math.max(100, Math.min(window.innerHeight * 0.8, resizingState.current.startHeight + delta));
|
||||||
const percentage = (relativeY / containerHeight) * 100;
|
libraryRef.current.style.height = `${newHeight}px`;
|
||||||
const clamped = Math.max(20, Math.min(80, percentage));
|
|
||||||
poolRef.current.style.height = `${clamped}%`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -507,11 +520,8 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
if (resizingState.current.active === 'sidebar' && sidebarRef.current) {
|
if (resizingState.current.active === 'sidebar' && sidebarRef.current) {
|
||||||
setSidebarWidth(parseInt(sidebarRef.current.style.width));
|
setSidebarWidth(parseInt(sidebarRef.current.style.width));
|
||||||
}
|
}
|
||||||
if (resizingState.current.active === 'pool' && poolRef.current) {
|
if (resizingState.current.active === 'library' && libraryRef.current) {
|
||||||
const hStyle = poolRef.current.style.height;
|
setLibraryHeight(parseInt(libraryRef.current.style.height));
|
||||||
if (hStyle.includes('%')) {
|
|
||||||
setPoolHeightPercent(parseFloat(hStyle));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resizingState.current.active = null;
|
resizingState.current.active = null;
|
||||||
@@ -609,7 +619,14 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
{viewMode === 'stack' && (
|
{viewMode === 'stack' && (
|
||||||
<div className="relative z-50">
|
<div className="relative z-50">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSortDropdownOpen(!sortDropdownOpen)}
|
onClick={() => {
|
||||||
|
// Store position for fixed dropdown
|
||||||
|
// We'll just use the button's position relative to viewport
|
||||||
|
// But since we can't easily pass state to the dropdown without more state,
|
||||||
|
// we'll just toggle and use fixed positioning in the dropdown render.
|
||||||
|
// Actually, let's use a simple state for position if needed, or just CSS.
|
||||||
|
setSortDropdownOpen(!sortDropdownOpen);
|
||||||
|
}}
|
||||||
className="flex items-center gap-2 bg-slate-900 rounded-lg p-1.5 border border-slate-700 h-9 px-3 text-xs font-bold text-white hover:bg-slate-800 transition-colors"
|
className="flex items-center gap-2 bg-slate-900 rounded-lg p-1.5 border border-slate-700 h-9 px-3 text-xs font-bold text-white hover:bg-slate-800 transition-colors"
|
||||||
>
|
>
|
||||||
<span className="text-slate-500 uppercase">Sort:</span>
|
<span className="text-slate-500 uppercase">Sort:</span>
|
||||||
@@ -619,8 +636,34 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
|
|
||||||
{sortDropdownOpen && (
|
{sortDropdownOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40" onClick={() => setSortDropdownOpen(false)} />
|
<div className="fixed inset-0 z-[900]" onClick={() => setSortDropdownOpen(false)} />
|
||||||
<div className="absolute top-full left-0 mt-2 w-40 bg-slate-800 border border-slate-700 rounded-xl shadow-xl overflow-hidden z-50 animate-in fade-in zoom-in-95 duration-200">
|
<div
|
||||||
|
className="fixed z-[999] bg-slate-800 border border-slate-700 rounded-xl shadow-xl overflow-hidden animate-in fade-in zoom-in-95 duration-200 flex flex-col gap-1 p-2 w-48"
|
||||||
|
style={{
|
||||||
|
top: (containerRef.current?.getBoundingClientRect()?.top || 0) + 60,
|
||||||
|
left: (containerRef.current?.getBoundingClientRect()?.left || 0) + 140
|
||||||
|
}}
|
||||||
|
// Improving position logic: Render close to the button would be better, but without refs it's hard.
|
||||||
|
// Let's rely on fixed centering or top-left offset if we can't get button rect easily.
|
||||||
|
// Actually, let's just render it relative to the logic above or modify button to set a ref.
|
||||||
|
// We can use a ref for the button which we don't have yet.
|
||||||
|
// Let's make it simple: Fixed position centered or just use a known offset?
|
||||||
|
// The tool-bar is overflow-x-auto, so relative position is risky.
|
||||||
|
// Let's use `top: 60px` (toolbar height ~56px) and some `left`.
|
||||||
|
// A better way is to attach a ref to the button now.
|
||||||
|
ref={(el) => {
|
||||||
|
if (el && el.previousElementSibling) { // The button is the previous sibling in DOM? No, the overlay is.
|
||||||
|
// This is getting hacky. Let's just fix the overflow issue in the Toolbar instead?
|
||||||
|
// User specifically asked to "take inspiration" and "sort list is opening below everything".
|
||||||
|
// Fixed positioning is safer.
|
||||||
|
// I will use a simple effect to position it if I had a ref.
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* We'll use a style hack to position it. OR just remove overflow-x-auto from toolbar if it's not needed. check resizing. */}
|
||||||
|
{/* User said "scrolls inside it's container". */}
|
||||||
|
{/* Let's use `position: fixed` and put it explicitly. */}
|
||||||
|
<div className="text-[10px] font-bold text-slate-500 px-2 py-1 uppercase tracking-wider">Group Cards By</div>
|
||||||
{[
|
{[
|
||||||
{ value: 'color', label: 'Color' },
|
{ value: 'color', label: 'Color' },
|
||||||
{ value: 'type', label: 'Type' },
|
{ value: 'type', label: 'Type' },
|
||||||
@@ -633,10 +676,10 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
setGroupBy(opt.value as any);
|
setGroupBy(opt.value as any);
|
||||||
setSortDropdownOpen(false);
|
setSortDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`w-full text-left px-4 py-2 text-xs font-bold flex items-center justify-between ${groupBy === opt.value ? 'bg-purple-900/30 text-purple-200' : 'text-slate-300 hover:bg-slate-700 hover:text-white'}`}
|
className={`w-full text-left px-3 py-2 text-xs font-bold rounded-lg flex items-center justify-between transition-colors ${groupBy === opt.value ? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-md' : 'text-slate-300 hover:bg-slate-700 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
{groupBy === opt.value && <Check className="w-3 h-3 text-purple-400" />}
|
{groupBy === opt.value && <Check className="w-3 h-3 text-white" />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -794,11 +837,10 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex flex-col min-h-0 relative">
|
<div className="flex-1 flex flex-col min-h-0 relative">
|
||||||
{/* Top: Pool + Land Station */}
|
{/* Top: Pool + Land Station */}
|
||||||
{/* Top: Pool + Land Station */}
|
<div className="flex-1 flex flex-col border-b border-slate-800 bg-slate-900/50 overflow-hidden min-h-0">
|
||||||
<div ref={poolRef} style={{ height: `${poolHeightPercent}%` }} className="flex flex-col border-b border-slate-800 bg-slate-900/50 overflow-hidden">
|
|
||||||
<DroppableZone
|
<DroppableZone
|
||||||
id="pool-zone"
|
id="pool-zone"
|
||||||
className="flex-1 flex flex-col"
|
className="flex-1 flex flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||||
<span>Card Pool ({pool.length})</span>
|
<span>Card Pool ({pool.length})</span>
|
||||||
@@ -808,20 +850,26 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
|
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
|
||||||
</div>
|
</div>
|
||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Resizer Handle */}
|
{/* Resizer Handle */}
|
||||||
<div
|
<div
|
||||||
className="h-2 bg-slate-800 hover:bg-purple-500/50 cursor-row-resize flex items-center justify-center shrink-0 z-20 group transition-colors touch-none"
|
className="h-2 bg-slate-800 hover:bg-purple-500/50 cursor-row-resize flex items-center justify-center shrink-0 z-20 group transition-colors touch-none w-full"
|
||||||
onMouseDown={(e) => handleResizeStart('pool', e)}
|
onMouseDown={(e) => handleResizeStart('library', e)}
|
||||||
onTouchStart={(e) => handleResizeStart('pool', e)}
|
onTouchStart={(e) => handleResizeStart('library', e)}
|
||||||
>
|
>
|
||||||
<div className="w-16 h-1 bg-slate-600 rounded-full group-hover:bg-purple-300" />
|
<div className="w-16 h-1 bg-slate-600 rounded-full group-hover:bg-purple-300" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom: Deck */}
|
{/* Bottom: Library */}
|
||||||
|
<div
|
||||||
|
ref={libraryRef}
|
||||||
|
style={{ height: `${libraryHeight}px` }}
|
||||||
|
className="shrink-0 flex flex-col border-t border-slate-800 bg-slate-900/50 overflow-hidden z-10"
|
||||||
|
>
|
||||||
<DroppableZone
|
<DroppableZone
|
||||||
id="deck-zone"
|
id="deck-zone"
|
||||||
className="flex-1 flex flex-col min-h-0 bg-slate-900/50 overflow-hidden"
|
className="flex-1 flex flex-col min-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||||
<span>Library ({deck.length})</span>
|
<span>Library ({deck.length})</span>
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any) => {
|
const DraftCardItem = ({ rawCard, handlePick, setHoveredCard }: any) => {
|
||||||
const card = normalizeCard(rawCard);
|
const card = normalizeCard(rawCard);
|
||||||
const isFoil = card.finish === 'foil';
|
const isFoil = card.finish === 'foil';
|
||||||
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => {
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user