created a new reusable component for the card lsft side preview
Some checks failed
Build and Deploy / build (push) Failing after 1m12s

This commit is contained in:
2025-12-22 18:45:28 +01:00
parent f335b33cf9
commit ac21657bc7
6 changed files with 334 additions and 367 deletions

View File

@@ -0,0 +1,142 @@
import React, { useMemo } from 'react';
// Union type to support both Game cards and Draft cards
// Union type to support both Game cards and Draft cards
export type VisualCard = {
// Common properties that might be needed
id?: string;
instanceId?: string;
name?: string;
imageUrl?: string;
image?: string;
image_uris?: {
normal?: string;
large?: string;
png?: string;
art_crop?: string;
border_crop?: string;
crop?: string;
};
definition?: any; // Scryfall definition
card_faces?: any[];
tapped?: boolean;
faceDown?: boolean;
counters?: any[];
finish?: string;
// Loose typing for properties that might vary between Game and Draft models
power?: string | number;
toughness?: string | number;
manaCost?: string;
mana_cost?: string;
typeLine?: string;
type_line?: string;
oracleText?: string;
oracle_text?: string;
[key: string]: any; // Allow other properties loosely
};
interface CardVisualProps {
card: VisualCard;
viewMode?: 'normal' | 'cutout';
isFoil?: boolean; // Explicit foil styling override
className?: string;
style?: React.CSSProperties;
// Optional overlays
showCounters?: boolean;
children?: React.ReactNode;
}
export const CardVisual: React.FC<CardVisualProps> = ({
card,
viewMode = 'normal',
isFoil = false,
className,
style,
showCounters = true,
children
}) => {
const imageSrc = useMemo(() => {
// Robustly resolve Image Source based on viewMode
let src = card.imageUrl || card.image;
if (viewMode === 'cutout') {
// Priority 1: Local Cache (standard naming convention) - PREFERRED BY USER
if (card.definition?.set && card.definition?.id) {
src = `/cards/images/${card.definition.set}/crop/${card.definition.id}.jpg`;
}
// Priority 2: Direct Image URIs (if available) - Fallback
else if (card.image_uris?.art_crop || card.image_uris?.crop) {
src = card.image_uris.art_crop || card.image_uris.crop!;
}
// Priority 3: Deep Definition Data
else if (card.definition?.image_uris?.art_crop) {
src = card.definition.image_uris.art_crop;
}
else if (card.definition?.card_faces?.[0]?.image_uris?.art_crop) {
src = card.definition.card_faces[0].image_uris.art_crop;
}
// Priority 4: If card has a manually set image property that looks like a crop (less reliable)
// Fallback: If no crop found, src remains whatever it was (likely full)
} else {
// Normal / Full View
// Priority 1: Local Cache (standard naming convention) - PREFERRED
if (card.definition?.set && card.definition?.id) {
// Check if we want standard full image path
src = `/cards/images/${card.definition.set}/full/${card.definition.id}.jpg`;
}
// Priority 2: Direct Image URIs
else if (card.image_uris?.normal) {
src = card.image_uris.normal;
}
else if (card.definition?.image_uris?.normal) {
src = card.definition.image_uris.normal;
}
else if (card.card_faces?.[0]?.image_uris?.normal) {
src = card.card_faces[0].image_uris.normal;
}
}
return src;
}, [card, viewMode]);
// Counters logic (only for Game cards usually)
const totalCounters = useMemo(() => {
if (!card.counters) return 0;
return card.counters.map((c: any) => c.count).reduce((a: number, b: number) => a + b, 0);
}, [card.counters]);
return (
<div
className={`relative overflow-hidden ${className || ''}`}
style={style}
>
{!card.faceDown ? (
<img
src={imageSrc}
alt={card.name || 'Card'}
className="w-full h-full object-cover"
draggable={false}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-900 bg-opacity-90 bg-[url('https://c1.scryfall.com/file/scryfall-card-backs/large/59/597b79b3-7d77-4261-871a-60dd17403388.jpg')] bg-cover">
</div>
)}
{/* Foil Overlay */}
{(isFoil || card.finish === 'foil') && !card.faceDown && (
<div className="absolute inset-0 pointer-events-none mix-blend-overlay bg-gradient-to-tr from-purple-500/30 via-transparent to-emerald-500/30 opacity-50" />
)}
{/* Counters */}
{showCounters && totalCounters > 0 && (
<div className="absolute top-1 right-1 bg-black/70 text-white text-xs px-1 rounded z-10 pointer-events-none">
{totalCounters}
</div>
)}
{children}
</div>
);
};

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { CardVisual, VisualCard } from './CardVisual';
import { Eye, ChevronLeft } from 'lucide-react';
import { ManaIcon } from './ManaIcon';
import { formatOracleText } from '../utils/textUtils';
interface SidePanelPreviewProps {
card: VisualCard | null;
width: number;
isCollapsed: boolean;
onToggleCollapse: (collapsed: boolean) => void;
onResizeStart?: (e: React.MouseEvent | React.TouchEvent) => void;
className?: string; // For additional styling (positioning, z-index, etc)
}
export const SidePanelPreview: React.FC<SidePanelPreviewProps> = ({
card,
width,
isCollapsed,
onToggleCollapse,
onResizeStart,
className,
children
}) => {
// If collapsed, render the collapsed strip
if (isCollapsed) {
return (
<div className={`flex shrink-0 w-12 flex-col items-center py-4 bg-slate-900 border-r border-slate-800 z-30 gap-4 transition-all duration-300 ${className || ''}`}>
<button
onClick={() => onToggleCollapse(false)}
className="p-3 rounded-xl transition-all duration-200 group relative text-slate-500 hover:text-purple-400 hover:bg-slate-800"
title="Expand Preview"
>
<Eye className="w-6 h-6" />
<span className="absolute left-full ml-3 top-1/2 -translate-y-1/2 bg-slate-800 text-white text-xs font-bold px-2 py-1 rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none ring-1 ring-white/10 z-50">
Card Preview
</span>
</button>
</div>
);
}
// Expanded View
return (
<div
className={`flex shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-30 p-4 relative group/sidebar shadow-2xl ${className || ''}`}
style={{ width: width }}
>
{/* Collapse Button */}
<button
onClick={() => onToggleCollapse(true)}
className="absolute top-2 right-2 p-1.5 bg-slate-800/80 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors z-20 opacity-0 group-hover/sidebar:opacity-100"
title="Collapse Preview"
>
<ChevronLeft className="w-4 h-4" />
</button>
{/* 3D Card Container */}
<div className="w-full relative sticky top-4 flex flex-col h-full overflow-hidden">
<div className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out shrink-0">
<div
className="relative w-full h-full"
style={{
transformStyle: 'preserve-3d',
transform: card ? 'rotateY(0deg)' : 'rotateY(180deg)',
transition: 'transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1)'
}}
>
{/* Front Face */}
<div
className="absolute inset-0 w-full h-full bg-slate-900 rounded-xl"
style={{ backfaceVisibility: 'hidden' }}
>
{card && (
<CardVisual
card={card}
viewMode="normal"
className="w-full h-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
// Pass specific foil prop if your card object uses different property keys or logic
// VisualCard handles `card.finish` internally too
/>
)}
</div>
{/* Back Face */}
<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"
draggable={false}
/>
</div>
</div>
</div>
{/* Details Section */}
{card && (
<div className="mt-4 flex-1 overflow-y-auto px-1 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
<h3 className="text-lg font-bold text-slate-200 leading-tight">{card.name}</h3>
{/* Mana Cost */}
{(card['manaCost'] || (card as any).mana_cost) && (
<div className="mt-1 flex items-center text-slate-400">
{((card['manaCost'] || (card as any).mana_cost) as string).match(/\{([^}]+)\}/g)?.map((s, i) => {
const sym = s.replace(/[{}]/g, '').toLowerCase().replace('/', '');
return <ManaIcon key={i} symbol={sym} shadow className="text-base mr-0.5" />;
}) || <span className="font-mono">{card['manaCost'] || (card as any).mana_cost}</span>}
</div>
)}
{/* Type Line */}
{(card['typeLine'] || (card as any).type_line) && (
<div className="text-xs text-emerald-400 uppercase tracking-wider font-bold mt-2 border-b border-white/10 pb-2 mb-3">
{card['typeLine'] || (card as any).type_line}
</div>
)}
{/* Oracle Text */}
{(card['oracleText'] || (card as any).oracle_text) && (
<div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 leading-relaxed shadow-inner">
{formatOracleText(card['oracleText'] || (card as any).oracle_text)}
</div>
)}
</div>
)}
{children}
</div>
{/* Resize Handle */}
{onResizeStart && (
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-emerald-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors touch-none"
onMouseDown={onResizeStart}
onTouchStart={onResizeStart}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-emerald-400 transition-colors" />
</div>
)}
</div>
);
};

View File

@@ -1,8 +1,9 @@
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, ChevronLeft, Eye } 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 { SidePanelPreview } from '../../components/SidePanelPreview';
import { DraftCard } from '../../services/PackGeneratorService'; import { DraftCard } from '../../services/PackGeneratorService';
import { useCardTouch } from '../../utils/interaction'; import { useCardTouch } from '../../utils/interaction';
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
@@ -882,106 +883,19 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<div className="flex-1 flex overflow-hidden lg:flex-row flex-col"> <div className="flex-1 flex overflow-hidden lg:flex-row flex-col">
{/* Zoom Sidebar */} {/* Zoom Sidebar */}
{/* Collapsed State: Toolbar Column */} <SidePanelPreview
{/* Collapsed State: Toolbar Column */} card={hoveredCard || displayCard}
{isSidebarCollapsed ? ( width={sidebarWidth}
<div key="collapsed" className="hidden xl:flex shrink-0 w-12 flex-col items-center py-4 bg-slate-900 border-r border-slate-800 z-10 gap-4 transition-all duration-300"> isCollapsed={isSidebarCollapsed}
<button onToggleCollapse={setIsSidebarCollapsed}
onClick={() => setIsSidebarCollapsed(false)} onResizeStart={(e) => handleResizeStart('sidebar', e)}
className="p-3 rounded-xl transition-all duration-200 group relative text-slate-500 hover:text-purple-400 hover:bg-slate-800" >
title="Expand Preview" {/* Mana Curve at Bottom */}
> <div className="mt-auto w-full pt-4 border-t border-slate-800">
<Eye className="w-6 h-6" /> <div className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-2 text-center">Mana Curve</div>
<span className="absolute left-full ml-3 top-1/2 -translate-y-1/2 bg-slate-800 text-white text-xs font-bold px-2 py-1 rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none ring-1 ring-white/10 z-50"> <ManaCurve deck={deck} />
Card Preview
</span>
</button>
</div> </div>
) : ( </SidePanelPreview>
<div
key="expanded"
ref={sidebarRef}
className="hidden xl:flex shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-10 p-4 relative group/sidebar"
style={{ perspective: '1000px', width: sidebarWidth }}
>
{/* Collapse Button */}
<button
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 right-2 p-1.5 bg-slate-800/80 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors z-20 opacity-0 group-hover/sidebar:opacity-100"
title="Collapse Preview"
>
<ChevronLeft className="w-4 h-4" />
</button>
{/* Front content ... */}
<div className="w-full relative 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">
<img
src={(hoveredCard || displayCard).image || (hoveredCard || displayCard).image_uris?.normal || (hoveredCard || displayCard).card_faces?.[0]?.image_uris?.normal}
alt={(hoveredCard || displayCard).name}
className="w-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
draggable={false}
/>
<div className="mt-4 text-center">
<h3 className="text-lg font-bold text-slate-200">{(hoveredCard || displayCard).name}</h3>
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{(hoveredCard || displayCard).typeLine || (hoveredCard || displayCard).type_line}</p>
{(hoveredCard || displayCard).oracle_text && (
<div className="mt-4 text-xs text-slate-400 text-left bg-slate-950 p-3 rounded-lg border border-slate-800 leading-relaxed max-h-60 overflow-y-auto custom-scrollbar">
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-1">{line}</p>)}
</div>
)}
</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"
draggable={false}
/>
</div>
</div>
</div>
{/* Mana Curve at Bottom */}
<div className="mt-auto w-full pt-4 border-t border-slate-800">
<div className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-2 text-center">Mana Curve</div>
<ManaCurve deck={deck} />
</div>
{/* Resize Handle */}
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-purple-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors touch-none"
onMouseDown={(e) => handleResizeStart('sidebar', e)}
onTouchStart={(e) => handleResizeStart('sidebar', e)}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-purple-400 transition-colors" />
</div>
</div>
)}
{/* Content Area */} {/* Content Area */}
{layout === 'vertical' ? ( {layout === 'vertical' ? (

View File

@@ -1,9 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { socketService } from '../../services/SocketService'; import { socketService } from '../../services/SocketService';
import { LogOut, Columns, LayoutTemplate, ChevronLeft, Eye } from 'lucide-react'; import { LogOut, Columns, LayoutTemplate } from 'lucide-react';
import { Modal } from '../../components/Modal'; import { Modal } from '../../components/Modal';
import { FoilOverlay, FloatingPreview } from '../../components/CardPreview'; import { FoilOverlay, FloatingPreview } from '../../components/CardPreview';
import { SidePanelPreview } from '../../components/SidePanelPreview';
import { useCardTouch } from '../../utils/interaction'; import { useCardTouch } from '../../utils/interaction';
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
@@ -373,96 +374,15 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
{/* Dedicated Zoom Zone (Left Sidebar) */} {/* Dedicated Zoom Zone (Left Sidebar) */}
{/* Collapsed State: Toolbar Column */} {/* Collapsed State: Toolbar Column */}
{isSidebarCollapsed ? ( {/* Dedicated Zoom Zone (Left Sidebar) */}
<div key="collapsed" className="hidden lg:flex shrink-0 w-12 flex-col items-center py-4 bg-slate-900 border-r border-slate-800/50 backdrop-blur-sm z-10 gap-4 transition-all duration-300"> <SidePanelPreview
<button card={hoveredCard || displayCard}
onClick={() => setIsSidebarCollapsed(false)} width={sidebarWidth}
className="p-3 rounded-xl transition-all duration-200 group relative text-slate-500 hover:text-purple-400 hover:bg-slate-800" isCollapsed={isSidebarCollapsed}
title="Expand Preview" onToggleCollapse={setIsSidebarCollapsed}
> onResizeStart={(e) => handleResizeStart('sidebar', e)}
<Eye className="w-6 h-6" /> className="hidden lg:flex"
<span className="absolute left-full ml-3 top-1/2 -translate-y-1/2 bg-slate-800 text-white text-xs font-bold px-2 py-1 rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none ring-1 ring-white/10 z-50"> />
Card Preview
</span>
</button>
</div>
) : (
<div
key="expanded"
ref={sidebarRef}
className="hidden lg:flex 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 relative group/sidebar"
style={{ perspective: '1000px', width: `${sidebarWidth}px` }}
>
{/* Collapse Button */}
<button
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 right-2 p-1.5 bg-slate-800/80 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors z-20 opacity-0 group-hover/sidebar:opacity-100"
title="Collapse Preview"
>
<ChevronLeft className="w-4 h-4" />
</button>
<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">
<img
src={(hoveredCard || displayCard).image || (hoveredCard || displayCard).image_uris?.normal || (hoveredCard || displayCard).card_faces?.[0]?.image_uris?.normal}
alt={(hoveredCard || displayCard).name}
className="w-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
draggable={false}
/>
<div className="mt-4 text-center">
<h3 className="text-lg font-bold text-slate-200">{(hoveredCard || displayCard).name}</h3>
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{(hoveredCard || displayCard).typeLine || (hoveredCard || displayCard).type_line}</p>
{(hoveredCard || displayCard).oracle_text && (
<div className="mt-4 text-xs text-slate-400 text-left bg-slate-950 p-3 rounded-lg border border-slate-800 leading-relaxed max-h-60 overflow-y-auto custom-scrollbar">
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-1">{line}</p>)}
</div>
)}
</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"
draggable={false}
/>
</div>
</div>
</div>
{/* Resize Handle for Sidebar */}
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-emerald-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors"
onMouseDown={(e) => handleResizeStart('sidebar', e)}
onTouchStart={(e) => handleResizeStart('sidebar', e)}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-emerald-400 transition-colors" />
</div>
</div>
)}
{/* Main Content Area: Handles both Pack and Pool based on layout */} {/* Main Content Area: Handles both Pack and Pool based on layout */}
{layout === 'vertical' ? ( {layout === 'vertical' ? (

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { CardInstance } from '../../types/game'; import { CardInstance } from '../../types/game';
import { useGesture } from './GestureManager'; import { useGesture } from './GestureManager';
import { useRef, useEffect } from 'react'; import { useRef, useEffect } from 'react';
import { CardVisual } from '../../components/CardVisual';
interface CardComponentProps { interface CardComponentProps {
card: CardInstance; card: CardInstance;
@@ -29,41 +30,7 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
return () => unregisterCard(card.instanceId); return () => unregisterCard(card.instanceId);
}, [card.instanceId]); }, [card.instanceId]);
// Robustly resolve Image Source based on viewMode // Robustly resolve Image Source based on viewMode is now handled in CardVisual
let imageSrc = card.imageUrl;
if (viewMode === 'cutout') {
// Priority 1: Local Cache (standard naming convention) - PREFERRED BY USER
if (card.definition?.set && card.definition?.id) {
imageSrc = `/cards/images/${card.definition.set}/crop/${card.definition.id}.jpg`;
}
// Priority 2: Direct Image URIs (if available) - Fallback
else if (card.image_uris?.art_crop || card.image_uris?.crop) {
imageSrc = card.image_uris.art_crop || card.image_uris.crop!;
}
// Priority 3: Deep Definition Data
else if (card.definition?.image_uris?.art_crop) {
imageSrc = card.definition.image_uris.art_crop;
}
else if (card.definition?.card_faces?.[0]?.image_uris?.art_crop) {
imageSrc = card.definition.card_faces[0].image_uris.art_crop;
}
// Fallback: If no crop found, imageSrc remains card.imageUrl (likely full)
} else {
// Normal / Full View
// Priority 1: Local Cache (standard naming convention) - PREFERRED
if (card.definition?.set && card.definition?.id) {
// Check if we want standard full image path
imageSrc = `/cards/images/${card.definition.set}/full/${card.definition.id}.jpg`;
}
// Priority 2: Direct Image URIs
else if (card.image_uris?.normal) {
imageSrc = card.image_uris.normal;
}
else if (card.definition?.image_uris?.normal) {
imageSrc = card.definition.image_uris.normal;
}
}
return ( return (
<div <div
@@ -98,25 +65,15 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
`} `}
style={style} style={style}
> >
<div className="w-full h-full relative overflow-hidden rounded-lg bg-slate-800 border-2 border-slate-700"> <div className="w-full h-full relative rounded-lg bg-slate-800 border-2 border-slate-700">
{!card.faceDown ? ( <CardVisual
<img card={card}
src={imageSrc} viewMode={viewMode}
alt={card.name} className="w-full h-full rounded-lg"
className="w-full h-full object-cover" />
draggable={false}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-900 bg-opacity-90 bg-[url('https://c1.scryfall.com/file/scryfall-card-backs/large/59/597b79b3-7d77-4261-871a-60dd17403388.jpg')] bg-cover">
</div>
)}
{/* Counters / PowerToughness overlays can go here */}
{(card.counters.length > 0) && (
<div className="absolute top-1 right-1 bg-black/70 text-white text-xs px-1 rounded">
{card.counters.map(c => c.count).reduce((a, b) => a + b, 0)}
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useConfirm } from '../../components/ConfirmDialog'; import { useConfirm } from '../../components/ConfirmDialog';
import { ChevronLeft, Eye, RotateCcw } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { ManaIcon } from '../../components/ManaIcon'; import { ManaIcon } from '../../components/ManaIcon';
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
@@ -16,7 +16,7 @@ import { GestureManager } from './GestureManager';
import { MulliganView } from './MulliganView'; import { MulliganView } from './MulliganView';
import { RadialMenu, RadialOption } from './RadialMenu'; import { RadialMenu, RadialOption } from './RadialMenu';
import { InspectorOverlay } from './InspectorOverlay'; import { InspectorOverlay } from './InspectorOverlay';
import { formatOracleText } from '../../utils/textUtils'; import { SidePanelPreview } from '../../components/SidePanelPreview';
// --- DnD Helpers --- // --- DnD Helpers ---
const DraggableCardWrapper = ({ children, card, disabled }: { children: React.ReactNode, card: CardInstance, disabled?: boolean }) => { const DraggableCardWrapper = ({ children, card, disabled }: { children: React.ReactNode, card: CardInstance, disabled?: boolean }) => {
@@ -459,127 +459,13 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
} }
{/* Zoom Sidebar */} {/* Zoom Sidebar */}
{ <SidePanelPreview
isSidebarCollapsed ? ( card={hoveredCard}
<div key="collapsed" className="hidden xl:flex shrink-0 w-12 flex-col items-center py-4 bg-slate-900 border-r border-slate-800 z-30 gap-4 transition-all duration-300"> width={sidebarWidth}
<button isCollapsed={isSidebarCollapsed}
onClick={() => setIsSidebarCollapsed(false)} onToggleCollapse={setIsSidebarCollapsed}
className="p-3 rounded-xl transition-all duration-200 group relative text-slate-500 hover:text-purple-400 hover:bg-slate-800" onResizeStart={handleResizeStart}
title="Expand Preview" />
>
<Eye className="w-6 h-6" />
<span className="absolute left-full ml-3 top-1/2 -translate-y-1/2 bg-slate-800 text-white text-xs font-bold px-2 py-1 rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none ring-1 ring-white/10 z-50">
Card Preview
</span>
</button>
</div>
) : (
<div
key="expanded"
ref={sidebarRef}
className="hidden xl:flex shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-30 p-4 relative group/sidebar shadow-2xl"
style={{ width: sidebarWidth }}
>
{/* Collapse Button */}
<button
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 right-2 p-1.5 bg-slate-800/80 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors z-20 opacity-0 group-hover/sidebar:opacity-100"
title="Collapse Preview"
>
<ChevronLeft className="w-4 h-4" />
</button>
<div className="w-full relative sticky top-4 flex flex-col h-full overflow-hidden">
<div className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out shrink-0">
<div
className="relative w-full h-full"
style={{
transformStyle: 'preserve-3d',
transform: hoveredCard ? 'rotateY(0deg)' : 'rotateY(180deg)',
transition: 'transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1)'
}}
>
{/* Front Face (Hovered Card) */}
<div
className="absolute inset-0 w-full h-full bg-slate-900 rounded-xl"
style={{ backfaceVisibility: 'hidden' }}
>
{hoveredCard && (
<img
src={(() => {
if (hoveredCard.image_uris?.normal) {
return hoveredCard.image_uris.normal;
}
if (hoveredCard.definition?.set && hoveredCard.definition?.id) {
return `/cards/images/${hoveredCard.definition.set}/full/${hoveredCard.definition.id}.jpg`;
}
return hoveredCard.imageUrl;
})()}
alt={hoveredCard.name}
className="w-full h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
/>
)}
</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"
draggable={false}
/>
</div>
</div>
</div>
{/* Oracle Text & Details - Only when card is hovered */}
{hoveredCard && (
<div className="mt-4 flex-1 overflow-y-auto px-1 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
<h3 className="text-lg font-bold text-slate-200 leading-tight">{hoveredCard.name}</h3>
{hoveredCard.manaCost && (
<div className="mt-1 flex items-center text-slate-400">
{hoveredCard.manaCost.match(/\{([^}]+)\}/g)?.map((s, i) => {
const sym = s.replace(/[{}]/g, '').toLowerCase().replace('/', '');
return <ManaIcon key={i} symbol={sym} shadow className="text-base mr-0.5" />;
}) || <span className="font-mono">{hoveredCard.manaCost}</span>}
</div>
)}
{hoveredCard.typeLine && (
<div className="text-xs text-emerald-400 uppercase tracking-wider font-bold mt-2 border-b border-white/10 pb-2 mb-3">
{hoveredCard.typeLine}
</div>
)}
{hoveredCard.oracleText && (
<div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 leading-relaxed shadow-inner">
{formatOracleText(hoveredCard.oracleText)}
</div>
)}
</div>
)}
</div>
{/* Resize Handle */}
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-emerald-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors touch-none"
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-emerald-400 transition-colors" />
</div>
</div>
)
}
{/* Main Game Area */} {/* Main Game Area */}
<div className="flex-1 flex flex-col h-full relative"> <div className="flex-1 flex flex-col h-full relative">