created a new reusable component for the card lsft side preview
Some checks failed
Build and Deploy / build (push) Failing after 1m12s
Some checks failed
Build and Deploy / build (push) Failing after 1m12s
This commit is contained in:
142
src/client/src/components/CardVisual.tsx
Normal file
142
src/client/src/components/CardVisual.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
src/client/src/components/SidePanelPreview.tsx
Normal file
148
src/client/src/components/SidePanelPreview.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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' ? (
|
||||||
|
|||||||
@@ -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' ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user