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
Some checks failed
Build and Deploy / build (push) Failing after 52s
This commit is contained in:
@@ -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.
|
- [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.
|
||||||
|
- [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.
|
- [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,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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { DraftCard } from '../services/PackGeneratorService';
|
import { DraftCard } from '../services/PackGeneratorService';
|
||||||
import { FoilOverlay } from './CardPreview';
|
import { FoilOverlay, CardHoverWrapper } from './CardPreview';
|
||||||
|
|
||||||
interface StackViewProps {
|
interface StackViewProps {
|
||||||
cards: DraftCard[];
|
cards: DraftCard[];
|
||||||
cardWidth?: number;
|
cardWidth?: number;
|
||||||
onCardClick?: (card: DraftCard) => void;
|
onCardClick?: (card: DraftCard) => void;
|
||||||
onHover?: (card: DraftCard | null) => void;
|
onHover?: (card: DraftCard | null) => void;
|
||||||
|
disableHoverPreview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_ORDER = [
|
const CATEGORY_ORDER = [
|
||||||
@@ -21,7 +22,7 @@ const CATEGORY_ORDER = [
|
|||||||
'Other'
|
'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 categorizedCards = useMemo(() => {
|
||||||
const categories: Record<string, DraftCard[]> = {};
|
const categories: Record<string, DraftCard[]> = {};
|
||||||
@@ -86,6 +87,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
|
|||||||
onMouseLeave={() => onHover && onHover(null)}
|
onMouseLeave={() => onHover && onHover(null)}
|
||||||
onClick={() => onCardClick && onCardClick(card)}
|
onClick={() => onCardClick && onCardClick(card)}
|
||||||
>
|
>
|
||||||
|
<CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 200}>
|
||||||
<div
|
<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`}
|
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={{
|
style={{
|
||||||
@@ -99,6 +101,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
|
|||||||
{/* Optional: Shine effect for foils if visible? */}
|
{/* Optional: Shine effect for foils if visible? */}
|
||||||
{card.finish === 'foil' && <FoilOverlay />}
|
{card.finish === 'foil' && <FoilOverlay />}
|
||||||
</div>
|
</div>
|
||||||
|
</CardHoverWrapper>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const CardsDisplay: React.FC<{
|
|||||||
cardWidth={cardWidth}
|
cardWidth={cardWidth}
|
||||||
onCardClick={(c) => onCardClick(c)}
|
onCardClick={(c) => onCardClick(c)}
|
||||||
onHover={(c) => onHover(c)}
|
onHover={(c) => onHover(c)}
|
||||||
|
disableHoverPreview={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { socketService } from '../../services/SocketService';
|
import { socketService } from '../../services/SocketService';
|
||||||
import { LogOut } from 'lucide-react';
|
import { LogOut } from 'lucide-react';
|
||||||
import { Modal } from '../../components/Modal';
|
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 {
|
interface DraftViewProps {
|
||||||
draftState: any;
|
draftState: any;
|
||||||
@@ -76,16 +84,23 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
window.addEventListener('mousemove', resize);
|
document.addEventListener('mousemove', resize);
|
||||||
window.addEventListener('mouseup', stopResizing);
|
document.addEventListener('mouseup', stopResizing);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', resize);
|
document.removeEventListener('mousemove', resize);
|
||||||
window.removeEventListener('mouseup', stopResizing);
|
document.removeEventListener('mouseup', stopResizing);
|
||||||
};
|
};
|
||||||
}, [isResizing]);
|
}, [isResizing]);
|
||||||
|
|
||||||
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
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 activePack = draftState.players[currentPlayerId]?.activePack;
|
||||||
const pickedCards = draftState.players[currentPlayerId]?.pool || [];
|
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">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
|
||||||
{/* Dedicated Zoom Zone (Left Sidebar) */}
|
{/* 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">
|
<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' }}>
|
||||||
{hoveredCard ? (
|
<div className="w-full relative sticky top-8 px-6">
|
||||||
<div className="animate-in fade-in slide-in-from-left-4 duration-300 p-4 sticky top-4">
|
<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
|
<img
|
||||||
src={hoveredCard.image || hoveredCard.image_uris?.normal || hoveredCard.card_faces?.[0]?.image_uris?.normal}
|
src={(hoveredCard || displayCard).image || (hoveredCard || displayCard).image_uris?.normal || (hoveredCard || displayCard).card_faces?.[0]?.image_uris?.normal}
|
||||||
alt={hoveredCard.name}
|
alt={(hoveredCard || displayCard).name}
|
||||||
className="w-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
|
className="w-full h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 text-center">
|
{/* Foil Overlay for Preview */}
|
||||||
<h3 className="text-lg font-bold text-slate-200">{hoveredCard.name}</h3>
|
{((hoveredCard || displayCard).finish === 'foil') && <FoilOverlay />}
|
||||||
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{hoveredCard.type_line}</p>
|
|
||||||
|
<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>
|
</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>
|
</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 */}
|
{/* 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']">
|
<div className="flex-1 overflow-y-auto p-4 z-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
|
||||||
{!activePack ? (
|
{!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">
|
<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>
|
<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]">
|
<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
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
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)}
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
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
|
<img
|
||||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
src={card.image}
|
||||||
alt={card.name}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -245,7 +305,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
{pickedCards.map((card: any, idx: number) => (
|
{pickedCards.map((card: any, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={`${card.id}-${idx}`}
|
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)}
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user