feat: Add hover border effect to cards when they are in the battlefield zone.
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.r2hp08ujhtk"
|
||||
"revision": "0.sortnjvj4s8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
93
src/client/src/components/GameToast.tsx
Normal file
93
src/client/src/components/GameToast.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { AlertCircle, CheckCircle, Info, XCircle } from 'lucide-react';
|
||||
|
||||
type GameToastType = 'success' | 'error' | 'info' | 'warning' | 'game-event';
|
||||
|
||||
interface GameToast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: GameToastType;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface GameToastContextType {
|
||||
showGameToast: (message: string, type?: GameToastType, duration?: number) => void;
|
||||
}
|
||||
|
||||
const GameToastContext = createContext<GameToastContextType | undefined>(undefined);
|
||||
|
||||
export const useGameToast = () => {
|
||||
const context = useContext(GameToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useGameToast must be used within a GameToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const GameToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<GameToast[]>([]);
|
||||
|
||||
// Use a ref to keep track of timeouts so we can clear them (optional, but good practice)
|
||||
// For simplicity here, we just use the timeout inside the callback.
|
||||
|
||||
const showGameToast = useCallback((message: string, type: GameToastType = 'info', duration = 3000) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
setToasts((prev) => [...prev, { id, message, type, duration }]);
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, duration);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<GameToastContext.Provider value={{ showGameToast }}>
|
||||
{children}
|
||||
{/*
|
||||
Positioning:
|
||||
We want this to be distinct from the system toast (top-center).
|
||||
Let's put it top-center but slightly lower, OR bottom-center?
|
||||
User request: "dedicated toast".
|
||||
Let's try "Center Top" but with a very distinct style, possibly overlaying the game board directly.
|
||||
Actually, let's put it at the TOP CENTER, but occupying a dedicated space or just below the system header.
|
||||
|
||||
Using z-[1000] to ensure it's above everything in the game.
|
||||
*/}
|
||||
<div className="fixed top-20 left-1/2 -translate-x-1/2 z-[1000] flex flex-col gap-2 pointer-events-none w-full max-w-md px-4 items-center">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`
|
||||
pointer-events-auto
|
||||
flex items-center gap-3 px-6 py-3 rounded-full shadow-[0_0_15px_rgba(0,0,0,0.5)]
|
||||
animate-in slide-in-from-top-4 fade-in zoom-in-95 duration-200
|
||||
border backdrop-blur-md
|
||||
${toastStyles[toast.type]}
|
||||
`}
|
||||
>
|
||||
{getIcon(toast.type)}
|
||||
<span className="font-bold text-sm tracking-wide text-shadow-sm">{toast.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</GameToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const toastStyles: Record<GameToastType, string> = {
|
||||
success: 'bg-emerald-900/90 border-emerald-500/50 text-emerald-100',
|
||||
error: 'bg-red-900/90 border-red-500/50 text-red-100',
|
||||
warning: 'bg-amber-900/90 border-amber-500/50 text-amber-100',
|
||||
info: 'bg-slate-900/90 border-slate-500/50 text-slate-100',
|
||||
'game-event': 'bg-indigo-900/90 border-indigo-500/50 text-indigo-100',
|
||||
};
|
||||
|
||||
const getIcon = (type: GameToastType) => {
|
||||
switch (type) {
|
||||
case 'success': return <CheckCircle className="w-5 h-5 text-emerald-400" />;
|
||||
case 'error': return <XCircle className="w-5 h-5 text-red-400" />;
|
||||
case 'warning': return <AlertCircle className="w-5 h-5 text-amber-400" />;
|
||||
case 'info': return <Info className="w-5 h-5 text-blue-400" />;
|
||||
case 'game-event': return <Info className="w-5 h-5 text-indigo-400" />;
|
||||
}
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
|
||||
opacity: card.tapped ? 0.5 : style?.opacity ?? 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full relative 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.zone === 'battlefield' ? 'hover:border-slate-400' : ''}`}>
|
||||
<CardVisual
|
||||
card={card}
|
||||
viewMode={viewMode}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { socketService } from '../../services/SocketService';
|
||||
import { Users, LogOut, Copy, Check, MessageSquare, Send, Bell, BellOff, X, Bot, Layers } from 'lucide-react';
|
||||
import { useConfirm } from '../../components/ConfirmDialog';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { useToast } from '../../components/Toast';
|
||||
import { useGameToast, GameToastProvider } from '../../components/GameToast';
|
||||
import { GameView } from '../game/GameView';
|
||||
import { DraftView } from '../draft/DraftView';
|
||||
import { TournamentManager as TournamentView } from '../tournament/TournamentManager';
|
||||
@@ -42,7 +42,7 @@ interface GameRoomProps {
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPlayerId, initialGameState, initialDraftState, onExit }) => {
|
||||
const GameRoomContent: React.FC<GameRoomProps> = ({ room: initialRoom, currentPlayerId, initialGameState, initialDraftState, onExit }) => {
|
||||
// State
|
||||
const [room, setRoom] = useState<Room>(initialRoom);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -63,7 +63,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
});
|
||||
|
||||
// Services
|
||||
const { showToast } = useToast();
|
||||
const { showGameToast } = useGameToast();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
// Restored States
|
||||
@@ -100,14 +100,14 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
// 1. New Players
|
||||
curr.forEach(p => {
|
||||
if (!prev.find(old => old.id === p.id)) {
|
||||
showToast(`${p.name} (${p.role}) joined the room.`, 'info');
|
||||
showGameToast(`${p.name} (${p.role}) joined the room.`, 'info');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Left Players
|
||||
prev.forEach(p => {
|
||||
if (!curr.find(newP => newP.id === p.id)) {
|
||||
showToast(`${p.name} left the room.`, 'warning');
|
||||
showGameToast(`${p.name} left the room.`, 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,16 +116,16 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
const old = prev.find(o => o.id === p.id);
|
||||
if (old) {
|
||||
if (!old.isOffline && p.isOffline) {
|
||||
showToast(`${p.name} lost connection.`, 'error');
|
||||
showGameToast(`${p.name} lost connection.`, 'error');
|
||||
}
|
||||
if (old.isOffline && !p.isOffline) {
|
||||
showToast(`${p.name} reconnected!`, 'success');
|
||||
showGameToast(`${p.name} reconnected!`, 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
prevPlayersRef.current = curr;
|
||||
}, [room.players, notificationsEnabled, showToast]);
|
||||
}, [room.players, notificationsEnabled, showGameToast]);
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
@@ -189,7 +189,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
|
||||
// Also handle finish
|
||||
const handleTournamentFinished = (data: any) => {
|
||||
showToast(`Tournament Winner: ${data.winner.name}!`, 'success');
|
||||
showGameToast(`Tournament Winner: ${data.winner.name}!`, 'success');
|
||||
};
|
||||
|
||||
socket.on('draft_update', handleDraftUpdate);
|
||||
@@ -222,12 +222,22 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
// Only show error if it's for me, or maybe generic "Action Failed"
|
||||
if (data.userId && data.userId !== currentPlayerId) return; // Don't spam others errors?
|
||||
|
||||
showToast(data.message, 'error');
|
||||
if (data.userId && data.userId !== currentPlayerId) return; // Don't spam others errors?
|
||||
|
||||
showGameToast(data.message, 'error');
|
||||
};
|
||||
|
||||
const handleGameNotification = (data: { message: string, type?: 'info' | 'success' | 'warning' | 'error' }) => {
|
||||
showGameToast(data.message, data.type || 'info');
|
||||
};
|
||||
|
||||
socket.on('game_error', handleGameError);
|
||||
return () => { socket.off('game_error', handleGameError); };
|
||||
}, [currentPlayerId, showToast]);
|
||||
socket.on('game_notification', handleGameNotification);
|
||||
return () => {
|
||||
socket.off('game_error', handleGameError);
|
||||
socket.off('game_notification', handleGameNotification);
|
||||
};
|
||||
}, [currentPlayerId, showGameToast]);
|
||||
|
||||
const sendMessage = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -324,7 +334,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
onSubmit={(deck) => {
|
||||
socketService.socket.emit('match_ready', { matchId: preparingMatchId, deck });
|
||||
setPreparingMatchId(null);
|
||||
showToast("Deck ready! Waiting for game to start...", 'success');
|
||||
showGameToast("Deck ready! Waiting for game to start...", 'success');
|
||||
}}
|
||||
submitLabel="Ready for Match"
|
||||
/>;
|
||||
@@ -661,3 +671,11 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameRoom: React.FC<GameRoomProps> = (props) => {
|
||||
return (
|
||||
<GameToastProvider>
|
||||
<GameRoomContent {...props} />
|
||||
</GameToastProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,11 @@ import { EventEmitter } from 'events';
|
||||
export class GameManager extends EventEmitter {
|
||||
public games: Map<string, StrictGameState> = new Map();
|
||||
|
||||
// Helper to emit generic game notifications
|
||||
public notify(roomId: string, message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info', targetId?: string) {
|
||||
this.emit('game_notification', roomId, { message, type, targetId });
|
||||
}
|
||||
|
||||
createGame(gameId: string, players: { id: string; name: string; isBot?: boolean }[]): StrictGameState {
|
||||
|
||||
// Convert array to map
|
||||
@@ -199,6 +204,7 @@ export class GameManager extends EventEmitter {
|
||||
if (game.phase !== 'ending') {
|
||||
console.log(`[GameManager] Game Over. Winner: ${winner.name}`);
|
||||
this.emit('game_over', { gameId, winnerId: winner.id });
|
||||
this.notify(gameId, `Game Over! ${winner.name} wins!`, 'success');
|
||||
game.phase = 'ending'; // Mark as ending so we don't double emit
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user