feat: Add PWA support and implement drag-and-drop functionality for deck building.
All checks were successful
Build and Deploy / build (push) Successful in 1m28s
All checks were successful
Build and Deploy / build (push) Successful in 1m28s
This commit is contained in:
12
src/client/public/icon.svg
Normal file
12
src/client/public/icon.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="120" fill="#0F172A"/>
|
||||
<path d="M128 128H384V384H128V128Z" fill="#1E293B" stroke="#10B981" stroke-width="24" stroke-linejoin="round"/>
|
||||
<path d="M168 168H424V424H168V168Z" fill="#0F172A" stroke="#A855F7" stroke-width="24" stroke-linejoin="round"/>
|
||||
<path d="M256 128V384M128 256H384" stroke="url(#paint0_radial)" stroke-width="4"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(256 256) rotate(90) scale(128)">
|
||||
<stop stop-color="#10B981"/>
|
||||
<stop offset="1" stop-color="#A855F7" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 738 B |
@@ -5,6 +5,8 @@ import { StackView } from '../../components/StackView';
|
||||
import { FoilOverlay } from '../../components/CardPreview';
|
||||
import { DraftCard } from '../../services/PackGeneratorService';
|
||||
import { useCardTouch } from '../../utils/interaction';
|
||||
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
interface DeckBuilderViewProps {
|
||||
roomId: string;
|
||||
@@ -17,10 +19,64 @@ interface DeckBuilderViewProps {
|
||||
const normalizeCard = (c: any): DraftCard => ({
|
||||
...c,
|
||||
finish: c.finish || 'nonfoil',
|
||||
typeLine: c.typeLine || c.type_line,
|
||||
// Ensure image is top-level for components that expect it
|
||||
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
|
||||
});
|
||||
|
||||
// Draggable Wrapper for Cards
|
||||
const DraggableCardWrapper = ({ children, card, source, disabled }: any) => {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: card.id,
|
||||
data: { card, source },
|
||||
disabled
|
||||
});
|
||||
|
||||
const style = transform ? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
opacity: isDragging ? 0 : 1,
|
||||
zIndex: isDragging ? 999 : undefined
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="touch-none">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Draggable Wrapper for Lands (Special case: ID is generic until dropped)
|
||||
const DraggableLandWrapper = ({ children, land }: any) => {
|
||||
const id = `land-source-${land.name}`;
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: id,
|
||||
data: { card: land, type: 'land' }
|
||||
});
|
||||
|
||||
// For lands, we want to copy, so don't hide original
|
||||
const style = transform ? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
zIndex: isDragging ? 999 : undefined,
|
||||
opacity: isDragging ? 0.5 : 1 // Show ghost
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="touch-none">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Droppable Zone
|
||||
const DroppableZone = ({ id, children, className }: any) => {
|
||||
const { setNodeRef, isOver } = useDroppable({ id });
|
||||
return (
|
||||
<div ref={setNodeRef} className={`${className} ${isOver ? 'ring-2 ring-emerald-500 bg-emerald-900/10' : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Reusable List Item Component
|
||||
const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c: any) => void }> = ({ card, onClick, onHover }) => {
|
||||
const isFoil = (card: DraftCard) => card.finish === 'foil';
|
||||
@@ -56,7 +112,7 @@ const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c:
|
||||
)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-[10px] text-slate-600 font-mono uppercase opacity-0 group-hover:opacity-100 transition-opacity">{card.type_line?.split('—')[0]?.trim()}</span>
|
||||
<span className="text-[10px] text-slate-600 font-mono uppercase opacity-0 group-hover:opacity-100 transition-opacity">{card.typeLine?.split('—')[0]?.trim()}</span>
|
||||
<span className={`w-2 h-2 rounded-full border ${getRarityColorClass(card.rarity)} !p-0 !text-[0px]`}></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +127,8 @@ const CardsDisplay: React.FC<{
|
||||
onCardClick: (c: any) => void;
|
||||
onHover: (c: any) => void;
|
||||
emptyMessage: string;
|
||||
}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage }) => {
|
||||
source: 'pool' | 'deck';
|
||||
}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage, source }) => {
|
||||
if (cards.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-500 opacity-50 p-8 border-2 border-dashed border-slate-700/50 rounded-lg">
|
||||
@@ -85,7 +142,11 @@ const CardsDisplay: React.FC<{
|
||||
const sorted = [...cards].sort((a, b) => (a.cmc || 0) - (b.cmc || 0));
|
||||
return (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
{sorted.map(c => <ListItem key={c.id} card={normalizeCard(c)} onClick={() => onCardClick(c)} onHover={onHover} />)}
|
||||
{sorted.map(c => (
|
||||
<DraggableCardWrapper key={c.id} card={c} source={source}>
|
||||
<ListItem card={normalizeCard(c)} onClick={() => onCardClick(c)} onHover={onHover} />
|
||||
</DraggableCardWrapper>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +154,8 @@ const CardsDisplay: React.FC<{
|
||||
if (viewMode === 'stack') {
|
||||
return (
|
||||
<div className="w-full h-full"> {/* Allow native scrolling from parent */}
|
||||
{/* StackView doesn't support DnD yet, so we disable it or handle it differently.
|
||||
For now, drag from StackView is not implemented, falling back to Click. */}
|
||||
<StackView
|
||||
cards={cards.map(normalizeCard)}
|
||||
cardWidth={cardWidth}
|
||||
@@ -119,14 +182,15 @@ const CardsDisplay: React.FC<{
|
||||
const isFoil = card.finish === 'foil';
|
||||
|
||||
return (
|
||||
<DeckCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
useArtCrop={useArtCrop}
|
||||
isFoil={isFoil}
|
||||
onCardClick={onCardClick}
|
||||
onHover={onHover}
|
||||
/>
|
||||
<DraggableCardWrapper key={card.id} card={card} source={source}>
|
||||
<DeckCardItem
|
||||
card={card}
|
||||
useArtCrop={useArtCrop}
|
||||
isFoil={isFoil}
|
||||
onCardClick={onCardClick}
|
||||
onHover={onHover}
|
||||
/>
|
||||
</DraggableCardWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -154,8 +218,10 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
|
||||
// --- Land Advice Logic ---
|
||||
const landSuggestion = useMemo(() => {
|
||||
// ... (logic remains same, simplified for brevity in thought but copied fully in implementation)
|
||||
const targetLands = 17;
|
||||
const existingLands = deck.filter(c => c.type_line && c.type_line.includes('Land')).length;
|
||||
// @ts-ignore
|
||||
const existingLands = deck.filter(c => (c.typeLine || c.type_line || '').includes('Land')).length;
|
||||
const landsNeeded = Math.max(0, targetLands - existingLands);
|
||||
|
||||
if (landsNeeded === 0) return null;
|
||||
@@ -164,7 +230,9 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
let totalPips = 0;
|
||||
|
||||
deck.forEach(card => {
|
||||
if (card.type_line && card.type_line.includes('Land')) return;
|
||||
// @ts-ignore
|
||||
const tLine = card.typeLine || card.type_line;
|
||||
if (tLine && tLine.includes('Land')) return;
|
||||
if (!card.mana_cost) return;
|
||||
const cost = card.mana_cost;
|
||||
pips.Plains += (cost.match(/{W}/g) || []).length;
|
||||
@@ -262,7 +330,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
id: `basic-${type}-${i}`,
|
||||
name: type,
|
||||
image_uris: { normal: landUrlMap[type] },
|
||||
type_line: "Basic Land"
|
||||
typeLine: "Basic Land"
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -274,7 +342,40 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
return [...(availableBasicLands || [])].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [availableBasicLands]);
|
||||
|
||||
// --- Render Functions (Inline) ---
|
||||
// --- DnD Handlers ---
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { distance: 10 } })
|
||||
);
|
||||
|
||||
const [draggedCard, setDraggedCard] = useState<any>(null);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
setDraggedCard(active.data.current?.card);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over) {
|
||||
setDraggedCard(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = active.data.current;
|
||||
if (!data) return;
|
||||
|
||||
if (data.type === 'land' && over.id === 'deck-zone') {
|
||||
addLandToDeck(data.card);
|
||||
} else if (data.source === 'pool' && over.id === 'deck-zone') {
|
||||
addToDeck(data.card);
|
||||
} else if (data.source === 'deck' && over.id === 'pool-zone') {
|
||||
removeFromDeck(data.card);
|
||||
}
|
||||
setDraggedCard(null);
|
||||
};
|
||||
|
||||
// --- Render Functions ---
|
||||
const renderLandStation = () => (
|
||||
<div className="bg-slate-900/40 rounded border border-slate-700/50 p-2 mb-2 shrink-0 flex flex-col gap-2">
|
||||
{/* Header & Advisor */}
|
||||
@@ -301,22 +402,24 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
{availableBasicLands && availableBasicLands.length > 0 ? (
|
||||
<div className="flex items-center gap-2 overflow-x-auto custom-scrollbar pb-1">
|
||||
{sortedLands.map((land) => (
|
||||
<div
|
||||
key={land.scryfallId}
|
||||
className="relative group cursor-pointer shrink-0"
|
||||
onClick={() => addLandToDeck(land)}
|
||||
onMouseEnter={() => setHoveredCard(land)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
>
|
||||
<img
|
||||
src={land.image || land.image_uris?.normal}
|
||||
className="w-16 rounded shadow group-hover:scale-105 transition-transform"
|
||||
alt={land.name}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 bg-black/40 rounded transition-opacity">
|
||||
<span className="text-white font-bold text-[10px] bg-black/50 px-1 rounded">+</span>
|
||||
<DraggableLandWrapper key={land.scryfallId} land={land}>
|
||||
<div
|
||||
className="relative group cursor-pointer shrink-0"
|
||||
onClick={() => addLandToDeck(land)}
|
||||
onMouseEnter={() => setHoveredCard(land)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
>
|
||||
<img
|
||||
src={land.image || land.image_uris?.normal}
|
||||
className="w-16 rounded shadow group-hover:scale-105 transition-transform"
|
||||
alt={land.name}
|
||||
draggable={false}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 bg-black/40 rounded transition-opacity">
|
||||
<span className="text-white font-bold text-[10px] bg-black/50 px-1 rounded">+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableLandWrapper>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -338,152 +441,164 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col select-none" onContextMenu={(e) => e.preventDefault()}>
|
||||
{/* Global Toolbar - Inlined */}
|
||||
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-4 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Layout Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setLayout('vertical')} className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Vertical Split"><Columns className="w-4 h-4" /></button>
|
||||
<button onClick={() => setLayout('horizontal')} className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Horizontal Split"><LayoutTemplate className="w-4 h-4" /></button>
|
||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
{/* Global Toolbar */}
|
||||
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-4 shrink-0 overflow-x-auto text-xs sm:text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Layout Switcher */}
|
||||
<div className="hidden sm:flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setLayout('vertical')} className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Vertical Split"><Columns className="w-4 h-4" /></button>
|
||||
<button onClick={() => setLayout('horizontal')} className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Horizontal Split"><LayoutTemplate className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setViewMode('list')} className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="List View"><List className="w-4 h-4" /></button>
|
||||
<button onClick={() => setViewMode('grid')} className={`p-1.5 rounded ${viewMode === 'grid' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Grid View"><LayoutGrid className="w-4 h-4" /></button>
|
||||
<button onClick={() => setViewMode('stack')} className={`p-1.5 rounded ${viewMode === 'stack' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Stack View"><Layers className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<div className="hidden sm:flex items-center gap-2 bg-slate-900 rounded-lg px-2 py-1 border border-slate-700 h-9">
|
||||
<div className="w-2 h-3 rounded border border-slate-500 bg-slate-700" />
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="300"
|
||||
step="1"
|
||||
value={cardWidth}
|
||||
onChange={(e) => setCardWidth(parseInt(e.target.value))}
|
||||
className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-800 rounded-lg appearance-none"
|
||||
/>
|
||||
<div className="w-3 h-5 rounded border border-slate-500 bg-slate-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||
<button onClick={() => setViewMode('list')} className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="List View"><List className="w-4 h-4" /></button>
|
||||
<button onClick={() => setViewMode('grid')} className={`p-1.5 rounded ${viewMode === 'grid' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Grid View"><LayoutGrid className="w-4 h-4" /></button>
|
||||
<button onClick={() => setViewMode('stack')} className={`p-1.5 rounded ${viewMode === 'stack' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Stack View"><Layers className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<div className="flex items-center gap-2 bg-slate-900 rounded-lg px-2 py-1 border border-slate-700 h-9">
|
||||
<div className="w-2 h-3 rounded border border-slate-500 bg-slate-700" />
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="300"
|
||||
step="1"
|
||||
value={cardWidth}
|
||||
onChange={(e) => setCardWidth(parseInt(e.target.value))}
|
||||
className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-800 rounded-lg appearance-none"
|
||||
/>
|
||||
<div className="w-3 h-5 rounded border border-slate-500 bg-slate-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-amber-400 font-mono text-sm font-bold bg-slate-900 px-3 py-1.5 rounded border border-amber-500/30">
|
||||
<Clock className="w-4 h-4" /> {formatTime(timer)}
|
||||
</div>
|
||||
<button
|
||||
onClick={submitDeck}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
|
||||
>
|
||||
<Save className="w-4 h-4" /> Submit Deck
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Zoom Sidebar */}
|
||||
<div className="hidden xl:flex w-72 shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-10 p-4" style={{ perspective: '1000px' }}>
|
||||
<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)'
|
||||
}}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden sm:flex items-center gap-2 text-amber-400 font-mono text-sm font-bold bg-slate-900 px-3 py-1.5 rounded border border-amber-500/30">
|
||||
<Clock className="w-4 h-4" /> {formatTime(timer)}
|
||||
</div>
|
||||
<button
|
||||
onClick={submitDeck}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
|
||||
>
|
||||
{/* 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"
|
||||
/>
|
||||
<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).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">
|
||||
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-1">{line}</p>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Save className="w-4 h-4" /> <span className="hidden sm:inline">Submit Deck</span><span className="sm:hidden">Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Back Face (Card Back) */}
|
||||
<div className="flex-1 flex overflow-hidden lg:flex-row flex-col">
|
||||
{/* Zoom Sidebar */}
|
||||
<div className="hidden xl:flex w-72 shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-10 p-4" style={{ perspective: '1000px' }}>
|
||||
<div className="w-full relative sticky top-4">
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full rounded-xl shadow-2xl overflow-hidden bg-slate-900"
|
||||
className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)'
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: hoveredCard ? 'rotateY(0deg)' : 'rotateY(180deg)'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/back.jpg"
|
||||
alt="Card Back"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* 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">
|
||||
{(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>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
{layout === 'vertical' ? (
|
||||
<div className="flex-1 flex flex-col lg:flex-row">
|
||||
{/* Pool Column */}
|
||||
<DroppableZone id="pool-zone" className="flex-1 flex flex-col min-w-0 border-r border-slate-800 bg-slate-900/50">
|
||||
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
|
||||
<span>Card Pool ({pool.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
{/* Deck Column */}
|
||||
<DroppableZone id="deck-zone" className="flex-1 flex flex-col min-w-0 bg-slate-900/50">
|
||||
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
|
||||
<span>Deck ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Deck is Empty" source="deck" />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Top: Pool + Land Station */}
|
||||
<DroppableZone id="pool-zone" className="flex-1 flex flex-col min-h-0 border-b border-slate-800 bg-slate-900/50">
|
||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||
<span>Card Pool ({pool.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
{/* Bottom: Deck */}
|
||||
<DroppableZone id="deck-zone" className="h-[40%] flex flex-col min-h-0 bg-slate-900/50">
|
||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||
<span>Deck ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Deck is Empty" source="deck" />
|
||||
</div>
|
||||
</DroppableZone>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
{layout === 'vertical' ? (
|
||||
<div className="flex-1 flex">
|
||||
{/* Pool Column */}
|
||||
<div className="flex-1 flex flex-col min-w-0 border-r border-slate-800 bg-slate-900/50">
|
||||
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
|
||||
<span>Card Pool ({pool.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" />
|
||||
</div>
|
||||
<DragOverlay dropAnimation={{ duration: 200, easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)' }}>
|
||||
{draggedCard ? (
|
||||
<div className={`w-36 rounded-xl shadow-2xl opacity-90 rotate-3 cursor-grabbing overflow-hidden ring-2 ring-emerald-500 bg-slate-900 aspect-[2.5/3.5]`}>
|
||||
<img src={draggedCard.image || draggedCard.image_uris?.normal} alt={draggedCard.name} className="w-full h-full object-cover" draggable={false} />
|
||||
</div>
|
||||
{/* Deck Column */}
|
||||
<div className="flex-1 flex flex-col min-w-0 bg-slate-900/50">
|
||||
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
|
||||
<span>Deck ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Deck is Empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Top: Pool + Land Station */}
|
||||
<div className="flex-1 flex flex-col min-h-0 border-b border-slate-800 bg-slate-900/50">
|
||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||
<span>Card Pool ({pool.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
|
||||
{renderLandStation()}
|
||||
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Bottom: Deck */}
|
||||
<div className="h-[40%] flex flex-col min-h-0 bg-slate-900/50">
|
||||
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
|
||||
<span>Deck ({deck.length})</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-2 custom-scrollbar">
|
||||
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Deck is Empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -500,13 +615,13 @@ const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) =
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onTouchMove={onTouchMove}
|
||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform touch-none"
|
||||
>
|
||||
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||
{isFoil && <FoilOverlay />}
|
||||
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
||||
{displayImage ? (
|
||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
|
||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LogOut, Columns, LayoutTemplate } from 'lucide-react';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { FoilOverlay } from '../../components/CardPreview';
|
||||
import { useCardTouch } from '../../utils/interaction';
|
||||
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
// Helper to normalize card data for visuals
|
||||
const normalizeCard = (c: any) => ({
|
||||
@@ -13,6 +15,19 @@ const normalizeCard = (c: any) => ({
|
||||
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
|
||||
});
|
||||
|
||||
// Droppable Wrapper for Pool
|
||||
const PoolDroppable = ({ children, className, style }: any) => {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: 'pool-zone',
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} className={`${className} ${isOver ? 'ring-4 ring-emerald-500/50 bg-emerald-900/20' : ''}`} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DraftViewProps {
|
||||
draftState: any;
|
||||
roomId: string; // Passed from parent
|
||||
@@ -108,267 +123,297 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
||||
const pickedCards = draftState.players[currentPlayerId]?.pool || [];
|
||||
|
||||
const handlePick = (cardId: string) => {
|
||||
// roomId and playerId are now inferred by the server from socket session
|
||||
socketService.socket.emit('pick_card', { cardId });
|
||||
};
|
||||
|
||||
// ... inside DraftView return ...
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { distance: 10 } })
|
||||
);
|
||||
|
||||
const [draggedCard, setDraggedCard] = useState<any>(null);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
setDraggedCard(active.data.current?.card);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (over && over.id === 'pool-zone') {
|
||||
handlePick(active.id as string);
|
||||
}
|
||||
setDraggedCard(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none" onContextMenu={(e) => e.preventDefault()}>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-black opacity-50 pointer-events-none"></div>
|
||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-black opacity-50 pointer-events-none"></div>
|
||||
|
||||
{/* Top Header: Timer & Pack Info */}
|
||||
<div className="shrink-0 p-4 z-10">
|
||||
<div className="flex justify-between items-center bg-slate-900/80 backdrop-blur border border-slate-800 p-4 rounded-lg shadow-lg">
|
||||
<div className="flex items-center gap-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500 shadow-amber-500/20 drop-shadow-sm">
|
||||
Pack {draftState.packNumber}
|
||||
</h2>
|
||||
<span className="text-sm text-slate-400 font-medium">Pick {pickedCards.length % 15 + 1}</span>
|
||||
</div>
|
||||
|
||||
{/* Layout Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700 h-10 items-center">
|
||||
<button
|
||||
onClick={() => setLayout('vertical')}
|
||||
className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`}
|
||||
title="Vertical Split"
|
||||
>
|
||||
<Columns className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLayout('horizontal')}
|
||||
className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`}
|
||||
title="Horizontal Split"
|
||||
>
|
||||
<LayoutTemplate className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card Scalar */}
|
||||
<div className="flex flex-col gap-1 w-24 md:w-32">
|
||||
<label className="text-[10px] text-slate-500 uppercase font-bold tracking-wider">Card Size</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
value={cardScale}
|
||||
onChange={(e) => setCardScale(parseFloat(e.target.value))}
|
||||
className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{!activePack ? (
|
||||
<div className="text-sm font-bold text-amber-500 animate-pulse uppercase tracking-wider">Waiting...</div>
|
||||
) : (
|
||||
<div className="text-4xl font-mono text-emerald-400 font-bold drop-shadow-[0_0_10px_rgba(52,211,153,0.5)]">
|
||||
00:{timer < 10 ? `0${timer}` : timer}
|
||||
{/* Top Header: Timer & Pack Info */}
|
||||
<div className="shrink-0 p-4 z-10">
|
||||
<div className="flex flex-col lg:flex-row justify-between items-center bg-slate-900/80 backdrop-blur border border-slate-800 p-4 rounded-lg shadow-lg gap-4 lg:gap-0">
|
||||
<div className="flex flex-wrap justify-center items-center gap-4 lg:gap-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500 shadow-amber-500/20 drop-shadow-sm">
|
||||
Pack {draftState.packNumber}
|
||||
</h2>
|
||||
<span className="text-sm text-slate-400 font-medium">Pick {pickedCards.length % 15 + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
{onExit && (
|
||||
<button
|
||||
onClick={() => setConfirmExitOpen(true)}
|
||||
className="p-3 bg-slate-800 hover:bg-red-500/20 text-slate-400 hover:text-red-500 border border-slate-700 hover:border-red-500/50 rounded-xl transition-all shadow-lg group"
|
||||
title="Exit to Lobby"
|
||||
>
|
||||
<LogOut className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Layout Switcher */}
|
||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700 h-10 items-center">
|
||||
<button
|
||||
onClick={() => setLayout('vertical')}
|
||||
className={`p-1.5 rounded ${layout === 'vertical' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`}
|
||||
title="Vertical Split"
|
||||
>
|
||||
<Columns className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLayout('horizontal')}
|
||||
className={`p-1.5 rounded ${layout === 'horizontal' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`}
|
||||
title="Horizontal Split"
|
||||
>
|
||||
<LayoutTemplate className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card Scalar */}
|
||||
<div className="flex flex-col gap-1 w-24 md:w-32">
|
||||
<label className="text-[10px] text-slate-500 uppercase font-bold tracking-wider">Card Size</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
value={cardScale}
|
||||
onChange={(e) => setCardScale(parseFloat(e.target.value))}
|
||||
className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{!activePack ? (
|
||||
<div className="text-sm font-bold text-amber-500 animate-pulse uppercase tracking-wider">Waiting...</div>
|
||||
) : (
|
||||
<div className="text-4xl font-mono text-emerald-400 font-bold drop-shadow-[0_0_10px_rgba(52,211,153,0.5)]">
|
||||
00:{timer < 10 ? `0${timer}` : timer}
|
||||
</div>
|
||||
)}
|
||||
{onExit && (
|
||||
<button
|
||||
onClick={() => setConfirmExitOpen(true)}
|
||||
className="p-3 bg-slate-800 hover:bg-red-500/20 text-slate-400 hover:text-red-500 border border-slate-700 hover:border-red-500/50 rounded-xl transition-all shadow-lg group"
|
||||
title="Exit to Lobby"
|
||||
>
|
||||
<LogOut className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Content: Zoom Sidebar + Pack Grid */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Middle Content: Zoom Sidebar + Pack Grid */}
|
||||
<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" 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) */}
|
||||
{/* 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" style={{ perspective: '1000px' }}>
|
||||
<div className="w-full relative sticky top-8 px-6">
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-slate-900 rounded-xl"
|
||||
style={{ backfaceVisibility: 'hidden' }}
|
||||
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)'
|
||||
}}
|
||||
>
|
||||
{(hoveredCard || displayCard) && (
|
||||
<div className="w-full h-full flex flex-col bg-slate-900 rounded-xl relative overflow-hidden">
|
||||
<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 h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
|
||||
/>
|
||||
{/* Foil Overlay for Preview */}
|
||||
{((hoveredCard || displayCard).finish === 'foil') && <FoilOverlay />}
|
||||
{/* 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 || 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"
|
||||
draggable={false}
|
||||
/>
|
||||
{/* 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 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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 Content Area: Handles both Pack and Pool based on layout */}
|
||||
{layout === 'vertical' ? (
|
||||
<div className="flex-1 flex min-w-0">
|
||||
{/* Left: Pack */}
|
||||
<div className="flex-1 overflow-y-auto p-4 z-0 custom-scrollbar border-r border-slate-800">
|
||||
{!activePack ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-full pb-10 fade-in animate-in duration-500">
|
||||
<div className="w-24 h-24 mb-6 relative">
|
||||
<div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
|
||||
<div className="absolute inset-0 rounded-full border-t-4 border-emerald-500 animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LogOut className="w-8 h-8 text-emerald-500 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">Waiting...</h2>
|
||||
<p className="text-slate-400">Your neighbor is picking.</p>
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{activePack.cards.map((rawCard: any) => (
|
||||
<DraftCardItem
|
||||
key={rawCard.id}
|
||||
rawCard={rawCard}
|
||||
cardScale={cardScale}
|
||||
handlePick={handlePick}
|
||||
setHoveredCard={setHoveredCard}
|
||||
/>
|
||||
))}
|
||||
</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 Content Area: Handles both Pack and Pool based on layout */}
|
||||
{layout === 'vertical' ? (
|
||||
<div className="flex-1 flex min-w-0">
|
||||
{/* Left: Pack */}
|
||||
<div className="flex-1 overflow-y-auto p-4 z-0 custom-scrollbar border-r border-slate-800">
|
||||
{!activePack ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-full pb-10 fade-in animate-in duration-500">
|
||||
<div className="w-24 h-24 mb-6 relative">
|
||||
<div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
|
||||
<div className="absolute inset-0 rounded-full border-t-4 border-emerald-500 animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LogOut className="w-8 h-8 text-emerald-500 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">Waiting...</h2>
|
||||
<p className="text-slate-400">Your neighbor is picking.</p>
|
||||
{/* Right: Pool (Vertical Column) */}
|
||||
<PoolDroppable className="flex-1 bg-slate-900/50 flex flex-col min-w-0 border-l border-slate-800 transition-colors duration-200">
|
||||
<div className="px-4 py-3 border-b border-slate-800 flex items-center justify-between shrink-0 bg-slate-900/80">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Your Pool ({pickedCards.length})
|
||||
</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex 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">
|
||||
{activePack.cards.map((rawCard: any) => (
|
||||
<DraftCardItem
|
||||
key={rawCard.id}
|
||||
rawCard={rawCard}
|
||||
cardScale={cardScale}
|
||||
handlePick={handlePick}
|
||||
setHoveredCard={setHoveredCard}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
||||
<div className="flex flex-wrap gap-4 content-start">
|
||||
{pickedCards.map((card: any, idx: number) => (
|
||||
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} vertical={true} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PoolDroppable>
|
||||
</div>
|
||||
|
||||
{/* Right: Pool (Vertical Column) */}
|
||||
<div className="flex-1 bg-slate-900/50 flex flex-col min-w-0 border-l border-slate-800">
|
||||
<div className="px-4 py-3 border-b border-slate-800 flex items-center justify-between shrink-0 bg-slate-900/80">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Your Pool ({pickedCards.length})
|
||||
</h3>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top: Pack */}
|
||||
<div className="flex-1 overflow-y-auto p-4 z-0 custom-scrollbar">
|
||||
{!activePack ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-full pb-10 fade-in animate-in duration-500">
|
||||
<div className="w-24 h-24 mb-6 relative">
|
||||
<div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
|
||||
<div className="absolute inset-0 rounded-full border-t-4 border-emerald-500 animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LogOut className="w-8 h-8 text-emerald-500 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">Waiting...</h2>
|
||||
<p className="text-slate-400">Your neighbor is picking.</p>
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{activePack.cards.map((rawCard: any) => (
|
||||
<DraftCardItem
|
||||
key={rawCard.id}
|
||||
rawCard={rawCard}
|
||||
cardScale={cardScale}
|
||||
handlePick={handlePick}
|
||||
setHoveredCard={setHoveredCard}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
||||
<div className="flex flex-wrap gap-4 content-start">
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className="h-1 bg-slate-800 hover:bg-emerald-500 cursor-row-resize z-30 transition-colors w-full flex items-center justify-center shrink-0"
|
||||
onMouseDown={startResizing}
|
||||
>
|
||||
<div className="w-16 h-1 bg-slate-600 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Pool (Horizontal Strip) */}
|
||||
<PoolDroppable
|
||||
className="shrink-0 bg-slate-900/90 backdrop-blur-md flex flex-col z-20 shadow-[-10px_-10px_30px_rgba(0,0,0,0.3)] transition-all ease-out duration-75 border-t border-slate-800"
|
||||
style={{ height: `${poolHeight}px` }}
|
||||
>
|
||||
<div className="px-6 py-2 flex items-center justify-between shrink-0">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Your Pool ({pickedCards.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar">
|
||||
{pickedCards.map((card: any, idx: number) => (
|
||||
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} vertical={true} />
|
||||
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top: Pack */}
|
||||
<div className="flex-1 overflow-y-auto p-4 z-0 custom-scrollbar">
|
||||
{!activePack ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-full pb-10 fade-in animate-in duration-500">
|
||||
<div className="w-24 h-24 mb-6 relative">
|
||||
<div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
|
||||
<div className="absolute inset-0 rounded-full border-t-4 border-emerald-500 animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LogOut className="w-8 h-8 text-emerald-500 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">Waiting...</h2>
|
||||
<p className="text-slate-400">Your neighbor is picking.</p>
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
{activePack.cards.map((rawCard: any) => (
|
||||
<DraftCardItem
|
||||
key={rawCard.id}
|
||||
rawCard={rawCard}
|
||||
cardScale={cardScale}
|
||||
handlePick={handlePick}
|
||||
setHoveredCard={setHoveredCard}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PoolDroppable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className="h-1 bg-slate-800 hover:bg-emerald-500 cursor-row-resize z-30 transition-colors w-full flex items-center justify-center shrink-0"
|
||||
onMouseDown={startResizing}
|
||||
>
|
||||
<div className="w-16 h-1 bg-slate-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={confirmExitOpen}
|
||||
onClose={() => setConfirmExitOpen(false)}
|
||||
title="Exit Draft?"
|
||||
message="Are you sure you want to exit the draft? You can rejoin later."
|
||||
type="warning"
|
||||
confirmLabel="Exit Draft"
|
||||
cancelLabel="Stay"
|
||||
onConfirm={onExit}
|
||||
/>
|
||||
|
||||
{/* Bottom: Pool (Horizontal Strip) */}
|
||||
<div
|
||||
className="shrink-0 bg-slate-900/90 backdrop-blur-md flex flex-col z-20 shadow-[-10px_-10px_30px_rgba(0,0,0,0.3)] transition-all ease-out duration-75 border-t border-slate-800"
|
||||
style={{ height: `${poolHeight}px` }}
|
||||
>
|
||||
<div className="px-6 py-2 flex items-center justify-between shrink-0">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Your Pool ({pickedCards.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar">
|
||||
{pickedCards.map((card: any, idx: number) => (
|
||||
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} />
|
||||
))}
|
||||
</div>
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{draggedCard ? (
|
||||
<div className="w-32 h-44 opacity-90 rotate-3 cursor-grabbing shadow-2xl">
|
||||
<img src={draggedCard.image} alt={draggedCard.name} className="w-full h-full object-cover rounded-xl" draggable={false} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={confirmExitOpen}
|
||||
onClose={() => setConfirmExitOpen(false)}
|
||||
title="Exit Draft?"
|
||||
message="Are you sure you want to exit the draft? You can rejoin later."
|
||||
type="warning"
|
||||
confirmLabel="Exit Draft"
|
||||
cancelLabel="Stay"
|
||||
onConfirm={onExit}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -378,10 +423,23 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any)
|
||||
const isFoil = card.finish === 'foil';
|
||||
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => handlePick(card.id), card);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: card.id,
|
||||
data: { card }
|
||||
});
|
||||
|
||||
const style = transform ? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
opacity: isDragging ? 0 : 1, // Hide original when dragging
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
||||
style={{ width: `${14 * cardScale}rem` }}
|
||||
ref={setNodeRef}
|
||||
style={{ ...style, width: `${14 * cardScale}rem` }}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer touch-none"
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHoveredCard(card)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
@@ -397,6 +455,7 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any)
|
||||
src={card.image}
|
||||
alt={card.name}
|
||||
className="w-full h-full object-cover relative z-10"
|
||||
draggable={false}
|
||||
/>
|
||||
{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>}
|
||||
@@ -422,6 +481,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
|
||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
||||
alt={card.name}
|
||||
className={`${vertical ? 'w-full h-full object-cover' : 'h-[90%] w-auto object-contain'} rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all`}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -51,6 +51,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
||||
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
|
||||
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game');
|
||||
|
||||
// Derived State
|
||||
const host = room.players.find(p => p.isHost);
|
||||
@@ -234,10 +235,33 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full gap-4">
|
||||
{renderContent()}
|
||||
<div className="flex h-full flex-col lg:flex-row gap-4 overflow-hidden">
|
||||
{/* Mobile Tab Bar */}
|
||||
<div className="lg:hidden shrink-0 flex items-center bg-slate-800 border-b border-slate-700">
|
||||
<button
|
||||
onClick={() => setMobileTab('game')}
|
||||
className={`flex-1 p-3 flex items-center justify-center gap-2 text-sm font-bold transition-colors ${mobileTab === 'game' ? 'text-emerald-400 bg-slate-700/50 border-b-2 border-emerald-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
>
|
||||
<Layers className="w-4 h-4" /> Game
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileTab('chat')}
|
||||
className={`flex-1 p-3 flex items-center justify-center gap-2 text-sm font-bold transition-colors ${mobileTab === 'chat' ? 'text-purple-400 bg-slate-700/50 border-b-2 border-purple-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-slate-600">/</span>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</div>
|
||||
Lobby & Chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-80 flex flex-col gap-4">
|
||||
<div className={`flex-1 min-h-0 flex flex-col ${mobileTab === 'game' ? 'flex' : 'hidden lg:flex'}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
<div className={`w-full lg:w-80 shrink-0 flex flex-col gap-4 min-h-0 ${mobileTab === 'chat' ? 'flex' : 'hidden lg:flex'}`}>
|
||||
<div className="flex-1 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl overflow-hidden flex flex-col">
|
||||
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
||||
<Users className="w-4 h-4" /> Lobby
|
||||
|
||||
4135
src/package-lock.json
generated
4135
src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,13 +11,17 @@
|
||||
"start": "NODE_ENV=production tsx server/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tsx": "^4.19.2"
|
||||
"tsx": "^4.19.2",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import * as path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['icon.svg'],
|
||||
manifest: {
|
||||
name: 'MTG Draft Maker',
|
||||
short_name: 'MTG Draft',
|
||||
description: 'Multiplayer Magic: The Gathering Draft Simulator',
|
||||
theme_color: '#0f172a',
|
||||
background_color: '#0f172a',
|
||||
display: 'standalone',
|
||||
orientation: 'any',
|
||||
start_url: '/',
|
||||
icons: [
|
||||
{
|
||||
src: 'icon.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
root: 'client', // Set root to client folder where index.html resides
|
||||
build: {
|
||||
outDir: '../dist', // Build to src/dist (outside client)
|
||||
|
||||
Reference in New Issue
Block a user