feat: Implement 3D flip card preview with foil effects in Draft View and add hover preview control to StackView.
Some checks failed
Build and Deploy / build (push) Failing after 52s

This commit is contained in:
2025-12-17 18:35:57 +01:00
parent 2869c35885
commit 3936260861
7 changed files with 167 additions and 55 deletions

View File

@@ -84,4 +84,7 @@
- [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.
- [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.
- [2025-12-17-183000_restore_stack_hover](./devlog/2025-12-17-183000_restore_stack_hover.md): Completed. Restored hover magnified card for Stack View in Pack Generation.
- [2025-12-17-183500_draft_ui_upgrade](./devlog/2025-12-17-183500_draft_ui_upgrade.md): Completed. Implemented 3D flip preview and consistent foil rendering in Draft View.
- [2025-12-17-184000_fix_draft_pool_ui](./devlog/2025-12-17-184000_fix_draft_pool_ui.md): Completed. Fixed "Your Pool" resizing bugs and removed unwanted hover animation.
- [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.

View File

@@ -0,0 +1,16 @@
# Restore Hover Magnified Card for Stack View
## Task
Restore the hover magnified card functionality for the stacked view in the pack generation UI, while ensuring it remains disabled for the deck building UI.
## Changes
- Modified `src/client/src/components/StackView.tsx`:
- Imported `CardHoverWrapper`.
- Added `disableHoverPreview` prop (default `false`).
- Wrapped card elements with `CardHoverWrapper`, passing `preventPreview` based on the new prop and card width.
- Modified `src/client/src/modules/draft/DeckBuilderView.tsx`:
- Passed `disableHoverPreview={true}` to `StackView` to maintain existing behavior for the deck builder (which uses a dedicated sidebar preview).
## Outcome
- Pack Generation UI (Cube Manager) now shows floating previews for cards in Stack View.
- Deck Builder UI remains unchanged (no double previews).

View File

@@ -0,0 +1,16 @@
# Draft Cards Picker UI Update
## Task
Update the `DraftView` to implement a 3D-flipping card preview sidebar (consistent with the Deck Builder) and ensure foil cards are rendered correctly in both the preview and the main selection grid.
## Changes
- Modified `src/client/src/modules/draft/DraftView.tsx`:
- Imported `FoilOverlay` component.
- Defined `normalizeCard` helper to standardise card object structure (handling images and finish).
- Added `displayCard` state to persist card details during the flip animation.
- Replaced the previous fade-in sidebar with the 3D perspective flip container from `DeckBuilderView`.
- Updated the card rendering loop in `activePack` to use `normalizeCard` and conditionally render `FoilOverlay` and foil-specific styles (purple glow, badges).
## Outcome
- **Sidebar**: Now features a persistent 3D flip animation. When hovering a card, it flips to show the front; when not hovering, it shows the card back (`/images/back.jpg`).
- **Foil Support**: Start using `FoilOverlay` for both the main draft grid and the sidebar preview, providing visual consistency for premium cards.

View File

@@ -0,0 +1,13 @@
# Fix Draft UI Issues
## Task
Fix bugs in the Draft Pick UI related to the "Your Pool" section.
## Changes
- Modified `src/client/src/modules/draft/DraftView.tsx`:
- **Resize Handle Fix**: Updated the resize event listeners (`mousemove`, `mouseup`) to be attached to `document` instead of `window`. This ensures the resize action continues smoothly even if the mouse leaves the browser window or moves rapidly over iframes/other elements that might swallow events.
- **Remove Hover Effect**: Removed the `hover:-translate-y-10` class from the drafted card items in the bottom pool view. The cards will now remain stationary on hover, as requested.
## Outcome
- The pool resizing experience is now consistent and does not lose focus when dragging quickly.
- Cards in the "Your Pool" strip no longer jump up when hovered, providing a stable viewing experience.

View File

@@ -1,12 +1,13 @@
import React, { useMemo } from 'react';
import { DraftCard } from '../services/PackGeneratorService';
import { FoilOverlay } from './CardPreview';
import { FoilOverlay, CardHoverWrapper } from './CardPreview';
interface StackViewProps {
cards: DraftCard[];
cardWidth?: number;
onCardClick?: (card: DraftCard) => void;
onHover?: (card: DraftCard | null) => void;
disableHoverPreview?: boolean;
}
const CATEGORY_ORDER = [
@@ -21,7 +22,7 @@ const CATEGORY_ORDER = [
'Other'
];
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover }) => {
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false }) => {
const categorizedCards = useMemo(() => {
const categories: Record<string, DraftCard[]> = {};
@@ -86,6 +87,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
onMouseLeave={() => onHover && onHover(null)}
onClick={() => onCardClick && onCardClick(card)}
>
<CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 200}>
<div
className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group-hover:ring-2 group-hover:ring-purple-400`}
style={{
@@ -99,6 +101,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
{/* Optional: Shine effect for foils if visible? */}
{card.finish === 'foil' && <FoilOverlay />}
</div>
</CardHoverWrapper>
</div>
)
})}

View File

@@ -92,6 +92,7 @@ const CardsDisplay: React.FC<{
cardWidth={cardWidth}
onCardClick={(c) => onCardClick(c)}
onHover={(c) => onHover(c)}
disableHoverPreview={true}
/>
</div>
)

View File

@@ -3,6 +3,14 @@ import React, { useState, useEffect } from 'react';
import { socketService } from '../../services/SocketService';
import { LogOut } from 'lucide-react';
import { Modal } from '../../components/Modal';
import { FoilOverlay } from '../../components/CardPreview';
// Helper to normalize card data for visuals
const normalizeCard = (c: any) => ({
...c,
finish: c.finish || 'nonfoil',
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
});
interface DraftViewProps {
draftState: any;
@@ -76,16 +84,23 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
};
if (isResizing) {
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResizing);
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResizing);
}
return () => {
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResizing);
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResizing);
};
}, [isResizing]);
const [hoveredCard, setHoveredCard] = useState<any>(null);
const [displayCard, setDisplayCard] = useState<any>(null);
useEffect(() => {
if (hoveredCard) {
setDisplayCard(normalizeCard(hoveredCard));
}
}, [hoveredCard]);
const activePack = draftState.players[currentPlayerId]?.activePack;
const pickedCards = draftState.players[currentPlayerId]?.pool || [];
@@ -152,29 +167,63 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
<div className="flex-1 flex overflow-hidden">
{/* Dedicated Zoom Zone (Left Sidebar) */}
<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 transition-all">
{hoveredCard ? (
<div className="animate-in fade-in slide-in-from-left-4 duration-300 p-4 sticky top-4">
<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 transition-all" style={{ perspective: '1000px' }}>
<div className="w-full relative sticky top-8 px-6">
<div
className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out"
style={{
transformStyle: 'preserve-3d',
transform: hoveredCard ? 'rotateY(0deg)' : 'rotateY(180deg)'
}}
>
{/* Front Face (Hovered Card) */}
<div
className="absolute inset-0 w-full h-full bg-slate-900 rounded-xl"
style={{ backfaceVisibility: 'hidden' }}
>
{(hoveredCard || displayCard) && (
<div className="w-full h-full flex flex-col bg-slate-900 rounded-xl relative overflow-hidden">
<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"
src={(hoveredCard || displayCard).image || (hoveredCard || displayCard).image_uris?.normal || (hoveredCard || displayCard).card_faces?.[0]?.image_uris?.normal}
alt={(hoveredCard || displayCard).name}
className="w-full h-full object-cover 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>
{/* Foil Overlay for Preview */}
{((hoveredCard || displayCard).finish === 'foil') && <FoilOverlay />}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-4 text-center z-20">
<h3 className="text-lg font-bold text-slate-200">{(hoveredCard || displayCard).name}</h3>
<p className="text-xs text-slate-300 uppercase tracking-wider mt-1">{(hoveredCard || displayCard).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>
{/* Back Face (Card Back) */}
<div
className="absolute inset-0 w-full h-full rounded-xl shadow-2xl overflow-hidden bg-slate-900"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
<img
src="/images/back.jpg"
alt="Card Back"
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Oracle Text Box Below Card */}
{(hoveredCard || displayCard)?.oracle_text && (
<div className={`mt-6 text-xs text-slate-300 text-left bg-slate-900/80 backdrop-blur p-4 rounded-lg border border-slate-700 leading-relaxed transition-opacity duration-300 ${hoveredCard ? 'opacity-100' : 'opacity-0'}`}>
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-2 last:mb-0">{line}</p>)}
</div>
)}
</div>
</div>
{/* Main Area: Current Pack OR Waiting State */}
<div className="flex-1 overflow-y-auto p-4 z-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
{!activePack ? (
@@ -198,7 +247,11 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
<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) => (
{activePack.cards.map((rawCard: any) => {
const card = normalizeCard(rawCard);
const isFoil = card.finish === 'foil';
return (
<div
key={card.id}
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
@@ -207,14 +260,21 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
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>
{/* Foil Glow Effect */}
{isFoil && <div className="absolute inset-0 -m-1 rounded-xl bg-purple-500 blur-md opacity-20 group-hover:opacity-60 transition-opacity duration-300 animate-pulse"></div>}
<div className={`relative w-full rounded-xl shadow-2xl shadow-black overflow-hidden bg-slate-900 ${isFoil ? 'ring-2 ring-purple-400/50' : 'group-hover:ring-2 ring-emerald-400/50'}`}>
<img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
src={card.image}
alt={card.name}
className="w-full rounded-xl shadow-2xl shadow-black group-hover:ring-2 ring-emerald-400/50 relative z-10"
className="w-full h-full object-cover relative z-10"
/>
{isFoil && <FoilOverlay />}
{isFoil && <div className="absolute top-2 right-2 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm border border-white/20">FOIL</div>}
</div>
))}
</div>
);
})}
</div>
</div>
)}
@@ -245,7 +305,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
{pickedCards.map((card: any, idx: number) => (
<div
key={`${card.id}-${idx}`}
className="relative group shrink-0 transition-all hover:-translate-y-10 h-full flex items-center"
className="relative group shrink-0 transition-all h-full flex items-center"
onMouseEnter={() => setHoveredCard(card)}
onMouseLeave={() => setHoveredCard(null)}
>