feat: Refactor application layout for full-height content and implement resizable draft UI with card zoom and scaling.

This commit is contained in:
2025-12-16 19:09:53 +01:00
parent 4663c968ee
commit ca76405986
12 changed files with 261 additions and 42 deletions

View File

@@ -13,3 +13,6 @@
- [Draft Rules & Pick Logic](./devlog/2025-12-16-180000_draft_rules_implementation.md): Completed. Enforced 4-player minimum and "Pick 2" rule for 4-player drafts. - [Draft Rules & Pick Logic](./devlog/2025-12-16-180000_draft_rules_implementation.md): Completed. Enforced 4-player minimum and "Pick 2" rule for 4-player drafts.
- [Fix Pack Duplication](./devlog/2025-12-16-184500_fix_pack_duplication.md): Completed. Enforced deep cloning and unique IDs for all draft packs to prevent opening identical packs. - [Fix Pack Duplication](./devlog/2025-12-16-184500_fix_pack_duplication.md): Completed. Enforced deep cloning and unique IDs for all draft packs to prevent opening identical packs.
- [Reconnection & Auto-Pick](./devlog/2025-12-16-191500_reconnection_and_autopick.md): Completed. Implemented session persistence, seamless reconnection, and 30s auto-pick on disconnect. - [Reconnection & Auto-Pick](./devlog/2025-12-16-191500_reconnection_and_autopick.md): Completed. Implemented session persistence, seamless reconnection, and 30s auto-pick on disconnect.
- [Draft Interface UI Polish](./devlog/2025-12-16-195000_draft_ui_polish.md): Completed. Redesigned the draft view for a cleaner, immersive, game-like experience with no unnecessary scrolls.
- [Resizable Draft Interface](./devlog/2025-12-16-200500_resizable_draft_ui.md): Completed. Implemented user-resizable pool panel and card sizes with persistence.
- [Card Zoom (Dedicated Zone)](./devlog/2025-12-16-203000_zoom_zone.md): Completed. Refactored layout to show zoomed card in a dedicated side panel.

View File

@@ -0,0 +1,23 @@
# Draft Interface UI Polish
## Status
- [x] Analyze current UI issues (bottom border, scrolling).
- [x] Remove global padding from `App.tsx`.
- [x] Refactor `DraftView.tsx` for a cleaner, game-like experience.
- [x] Implement immersive 3D effects and tray-style pool view.
## Context
The user requested to improve the draft card pick interface. Specifically to remove the ugly bottom border, avoid page scrolls, and make it feel more like a game.
## Implementation Details
### `src/client/src/App.tsx`
- Removed `pb-20` from the main container to allow full-screen layouts without forced scrolling at the bottom.
### `src/client/src/modules/draft/DraftView.tsx`
- **Layout**: Changed to relative positioning with `overflow-hidden` to contain all elements within the viewport.
- **Visuals**:
- Added a radial gradient background overlay.
- Redesigned the "Current Pack" area with `[perspective:1000px]` and 3D hover transforms.
- Redesigned the "Your Pool" bottom area to be a "tray" with `backdrop-blur`, gradient background, and removed the boxy border.
- **Scrollbars**: Hidden scrollbars in the main pack view for a cleaner look (`[&::-webkit-scrollbar]:hidden`).

View File

@@ -0,0 +1,34 @@
# Resizable Draft Interface
## Status
- [x] Implement resizable bottom "Pool" panel.
- [x] Implement resizable card size slider.
- [x] Persist settings to `localStorage`.
## Technical Plan
### `src/client/src/modules/draft/DraftView.tsx`
1. **State Initialization**:
- `poolHeight`: number (default ~220). Load from `localStorage.getItem('draft_poolHeight')`.
- `cardScale`: number (default 1 or specific width like 224px). Load from `localStorage.getItem('draft_cardScale')`.
2. **Resize Handle**:
- Insert a `div` cursor-row-resize between the Main Area and the Bottom Area.
- Implement `onMouseDown` handler to start dragging.
- Implement `onMouseMove` and `onMouseUp` on the window/document to handle the resize logic.
3. **Card Size Control**:
- Add a slider (`<input type="range" />`) in the Top Header area to adjust `cardScale`.
- Apply this scale to the card images/containers in the Main Area.
4. **Persistence**:
- `useEffect` hooks to save state changes to `localStorage`.
5. **Refactoring Styling**:
- Change `h-[220px]` class on the bottom panel to `style={{ height: poolHeight }}`.
- Update card width class `w-56` to dynamic style or class based on scale.
## UX Improvements
- Add limit constraints (min height for pool, max height for pool).
- Add limit constraints for card size (min visible, max huge).

View File

@@ -0,0 +1,13 @@
# Card Zoom on Hover
## Status
- [x] Add `hoveredCard` state to `DraftView`.
- [x] Implement `onMouseEnter`/`onMouseLeave` handlers for cards in both Pick and Pool areas.
- [x] rendering a fixed, high z-index preview of the hovered card.
- [x] Disable right-click context menu on Draft interface.
## Implementation Details
- **File**: `src/client/src/modules/draft/DraftView.tsx`
- **Zoom Component**: A fixed `div` containing the large card image.
- **Position**: Fixed to the left or right side of the screen (e.g., `left-10 top-1/2 -translate-y-1/2`) to avoid covering the grid being interacted with (which is usually centered).
- **Styling**: Large size (e.g., `w-80` or `h-[500px]`), shadow, border, rounded corners.

View File

@@ -0,0 +1,15 @@
# Card Zoom on Hover - Dedicated Zone
## Status
- [x] Add `hoveredCard` state to `DraftView` (Already done).
- [x] Implement `onMouseEnter`/`onMouseLeave` handlers (Already done).
- [x] Refactor `DraftView` layout to include a dedicated sidebar for card preview.
- [x] Move the zoomed card image into this dedicated zone instead of a fixed overlay.
## Implementation Details
- **File**: `src/client/src/modules/draft/DraftView.tsx`
- **Layout Change**:
- Wrap the central card selection area in a `flex-row` container.
- Add a Left Sidebar (e.g., `w-80`) reserved for the zoomed card.
- Ensure the main card grid takes up the remaining space (`flex-1`).
- **Behavior**: When no card is hovered, the sidebar can show a placeholder or remain empty/decorative.

View File

@@ -35,8 +35,8 @@ export const App: React.FC = () => {
}, [generatedPacks]); }, [generatedPacks]);
return ( return (
<div className="min-h-screen bg-slate-900 text-slate-100 font-sans pb-20"> <div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
<header className="bg-slate-800 border-b border-slate-700 p-4 sticky top-0 z-50 shadow-lg"> <header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4"> <div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div> <div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div>
@@ -75,7 +75,7 @@ export const App: React.FC = () => {
</div> </div>
</header> </header>
<main> <main className="flex-1 overflow-hidden relative">
{activeTab === 'draft' && ( {activeTab === 'draft' && (
<CubeManager <CubeManager
packs={generatedPacks} packs={generatedPacks}

View File

@@ -289,7 +289,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
}; };
return ( return (
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 p-4 md:p-6"> <div className="h-full overflow-y-auto max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 p-4 md:p-6">
{/* --- LEFT COLUMN: CONTROLS --- */} {/* --- LEFT COLUMN: CONTROLS --- */}
<div className="lg:col-span-4 flex flex-col gap-4"> <div className="lg:col-span-4 flex flex-col gap-4">

View File

@@ -18,6 +18,59 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); // Reset timer on new pack? Simplified for now. }, []); // Reset timer on new pack? Simplified for now.
// --- UI State & Persistence ---
const [poolHeight, setPoolHeight] = useState<number>(() => {
const saved = localStorage.getItem('draft_poolHeight');
return saved ? parseInt(saved, 10) : 220;
});
const [cardScale, setCardScale] = useState<number>(() => {
const saved = localStorage.getItem('draft_cardScale');
return saved ? parseFloat(saved) : 0.7;
});
const [isResizing, setIsResizing] = useState(false);
// Persist settings
useEffect(() => {
localStorage.setItem('draft_poolHeight', poolHeight.toString());
}, [poolHeight]);
useEffect(() => {
localStorage.setItem('draft_cardScale', cardScale.toString());
}, [cardScale]);
// Resize Handlers
const startResizing = (e: React.MouseEvent) => {
setIsResizing(true);
e.preventDefault();
};
useEffect(() => {
const stopResizing = () => setIsResizing(false);
const resize = (e: MouseEvent) => {
if (isResizing) {
const newHeight = window.innerHeight - e.clientY;
// Limits: Min 100px, Max 60% of screen
const maxHeight = window.innerHeight * 0.6;
if (newHeight >= 100 && newHeight <= maxHeight) {
setPoolHeight(newHeight);
}
}
};
if (isResizing) {
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResizing);
}
return () => {
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResizing);
};
}, [isResizing]);
const [hoveredCard, setHoveredCard] = useState<any>(null);
const activePack = draftState.players[currentPlayerId]?.activePack; const activePack = draftState.players[currentPlayerId]?.activePack;
const pickedCards = draftState.players[currentPlayerId]?.pool || []; const pickedCards = draftState.players[currentPlayerId]?.pool || [];
@@ -35,54 +88,132 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
} }
return ( return (
<div className="flex flex-col h-full bg-slate-950 text-white p-4 gap-4"> <div className="flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none" onContextMenu={(e) => e.preventDefault()}>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-black opacity-50 pointer-events-none"></div>
{/* Top Header: Timer & Pack Info */} {/* Top Header: Timer & Pack Info */}
<div className="flex justify-between items-center bg-slate-900 p-4 rounded-lg border border-slate-800"> <div className="shrink-0 p-4 z-10">
<div> <div className="flex justify-between items-center bg-slate-900/80 backdrop-blur border border-slate-800 p-4 rounded-lg shadow-lg">
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500"> <div className="flex items-center gap-8">
Pack {draftState.packNumber} <div>
</h2> <h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500 shadow-amber-500/20 drop-shadow-sm">
<span className="text-sm text-slate-400">Pick {pickedCards.length % 15 + 1}</span> Pack {draftState.packNumber}
</div> </h2>
<div className="text-3xl font-mono text-emerald-400 font-bold"> <span className="text-sm text-slate-400 font-medium">Pick {pickedCards.length % 15 + 1}</span>
00:{timer < 10 ? `0${timer}` : timer} </div>
{/* Card Scalar */}
<div className="hidden md:flex flex-col gap-1 w-32">
<label className="text-[10px] text-slate-500 uppercase font-bold tracking-wider">Card Size</label>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={cardScale}
onChange={(e) => setCardScale(parseFloat(e.target.value))}
className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/>
</div>
</div>
<div className="text-4xl font-mono text-emerald-400 font-bold drop-shadow-[0_0_10px_rgba(52,211,153,0.5)]">
00:{timer < 10 ? `0${timer}` : timer}
</div>
</div> </div>
</div> </div>
{/* Main Area: Current Pack */} {/* Middle Content: Zoom Sidebar + Pack Grid */}
<div className="flex-1 bg-slate-900/50 p-6 rounded-xl border border-slate-800 overflow-y-auto"> <div className="flex-1 flex overflow-hidden">
<h3 className="text-center text-slate-400 uppercase tracking-widest text-sm font-bold mb-6">Select a Card</h3>
<div className="flex flex-wrap justify-center gap-4"> {/* Dedicated Zoom Zone (Left Sidebar) */}
{activePack.cards.map((card: any) => ( <div className="hidden lg:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800/50 bg-slate-900/20 backdrop-blur-sm z-10">
{hoveredCard ? (
<div className="animate-in fade-in slide-in-from-left-4 duration-300 p-4 sticky top-4">
<img
src={hoveredCard.image || hoveredCard.image_uris?.normal || hoveredCard.card_faces?.[0]?.image_uris?.normal}
alt={hoveredCard.name}
className="w-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
/>
<div className="mt-4 text-center">
<h3 className="text-lg font-bold text-slate-200">{hoveredCard.name}</h3>
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{hoveredCard.type_line}</p>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-slate-600 p-8 text-center opacity-50">
<div className="w-48 h-64 border-2 border-dashed border-slate-700 rounded-xl mb-4 flex items-center justify-center">
<span className="text-xs uppercase font-bold tracking-widest">Hover Card</span>
</div>
<p className="text-sm">Hover over a card to view clear details.</p>
</div>
)}
</div>
{/* Main Area: Current Pack */}
<div className="flex-1 overflow-y-auto p-4 z-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
<div className="flex flex-col items-center justify-center min-h-full pb-10">
<h3 className="text-center text-slate-500 uppercase tracking-[0.2em] text-xs font-bold mb-8">Select a Card</h3>
<div className="flex flex-wrap justify-center gap-6 [perspective:1000px]">
{activePack.cards.map((card: any) => (
<div
key={card.id}
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
style={{ width: `${14 * cardScale}rem` }}
onClick={() => handlePick(card.id)}
onMouseEnter={() => setHoveredCard(card)}
onMouseLeave={() => setHoveredCard(null)}
>
<div className="absolute inset-0 rounded-xl bg-emerald-500 blur-xl opacity-0 group-hover:opacity-40 transition-opacity duration-300"></div>
<img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
alt={card.name}
className="w-full rounded-xl shadow-2xl shadow-black group-hover:ring-2 ring-emerald-400/50 relative z-10"
/>
</div>
))}
</div>
</div>
</div>
</div>
{/* Resize Handle */}
<div
className="h-1 bg-slate-800 hover:bg-emerald-500 cursor-row-resize z-30 transition-colors w-full flex items-center justify-center shrink-0"
onMouseDown={startResizing}
>
<div className="w-16 h-1 bg-slate-600 rounded-full"></div>
</div>
{/* Bottom Area: Drafted Pool Preview */}
<div
className="shrink-0 bg-gradient-to-t from-slate-950 to-slate-900/90 backdrop-blur-md flex flex-col z-20 shadow-[0_-10px_40px_rgba(0,0,0,0.5)] transition-all ease-out duration-75"
style={{ height: `${poolHeight}px` }}
>
<div className="px-6 py-2 flex items-center justify-between shrink-0">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
Your Pool ({pickedCards.length})
</h3>
</div>
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar">
{pickedCards.map((card: any, idx: number) => (
<div <div
key={card.id} key={`${card.id}-${idx}`}
className="group relative transition-all hover:scale-110 hover:z-10 cursor-pointer" className="relative group shrink-0 transition-all hover:-translate-y-10 h-full flex items-center"
onClick={() => handlePick(card.id)} onMouseEnter={() => setHoveredCard(card)}
onMouseLeave={() => setHoveredCard(null)}
> >
<img <img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal} src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
alt={card.name} alt={card.name}
className="w-48 rounded-lg shadow-xl shadow-black/50 group-hover:shadow-emerald-500/50 group-hover:ring-2 ring-emerald-400" className="h-[90%] w-auto rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all object-contain"
/> />
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Bottom Area: Drafted Pool Preview */}
<div className="h-48 bg-slate-900 p-4 rounded-lg border border-slate-800 flex flex-col">
<h3 className="text-xs font-bold text-slate-500 uppercase mb-2">Your Pool ({pickedCards.length})</h3>
<div className="flex-1 overflow-x-auto flex items-center gap-1 pb-2">
{pickedCards.map((card: any, idx: number) => (
<img
key={`${card.id}-${idx}`}
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
alt={card.name}
className="h-full rounded shadow-md"
/>
))}
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -247,7 +247,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
}; };
return ( return (
<div className="flex h-[calc(100vh-100px)] gap-4"> <div className="flex h-full gap-4">
{renderContent()} {renderContent()}
{/* Sidebar: Players & Chat */} {/* Sidebar: Players & Chat */}

View File

@@ -188,7 +188,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
} }
return ( return (
<div className="max-w-4xl mx-auto p-4 md:p-10"> <div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-10">
<div className="bg-slate-800 rounded-2xl p-8 border border-slate-700 shadow-2xl"> <div className="bg-slate-800 rounded-2xl p-8 border border-slate-700 shadow-2xl">
<h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-3"> <h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-3">
<Users className="w-8 h-8 text-purple-500" /> Multiplayer Lobby <Users className="w-8 h-8 text-purple-500" /> Multiplayer Lobby

View File

@@ -120,7 +120,7 @@ export const DeckTester: React.FC = () => {
} }
return ( return (
<div className="max-w-4xl mx-auto p-4 md:p-8"> <div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-8">
<div className="bg-slate-800 rounded-2xl p-8 border border-slate-700 shadow-2xl"> <div className="bg-slate-800 rounded-2xl p-8 border border-slate-700 shadow-2xl">
<h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-3"> <h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-3">
<Play className="w-8 h-8 text-emerald-500" /> Deck Tester <Play className="w-8 h-8 text-emerald-500" /> Deck Tester

View File

@@ -48,7 +48,7 @@ export const TournamentManager: React.FC = () => {
}; };
return ( return (
<div className="max-w-4xl mx-auto p-4 md:p-6"> <div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-6">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8"> <div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8">
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2"> <h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<Users className="w-5 h-5 text-blue-400" /> Players <Users className="w-5 h-5 text-blue-400" /> Players