feat: refactor lobby UI with collapsible panels, add player event notifications, and update card art crop threshold to 130px

This commit is contained in:
2025-12-18 01:38:28 +01:00
parent 851e2aa81d
commit ebfdfef5ae
8 changed files with 321 additions and 120 deletions

View File

@@ -98,3 +98,5 @@
- [Minimize Slider Defaults](./devlog/2025-12-18-013000_minimize_slider_defaults.md): Completed. Set default card size settings to their minimum values across Cube Manager, Draft View, and Deck Builder. - [Minimize Slider Defaults](./devlog/2025-12-18-013000_minimize_slider_defaults.md): Completed. Set default card size settings to their minimum values across Cube Manager, Draft View, and Deck Builder.
- [Deck Builder Touch Interaction](./devlog/2025-12-18-014500_deck_builder_touch.md): Completed. Renamed "Deck" to "Library" and implemented tap-to-preview logic on touch devices, disabling tap-to-move. - [Deck Builder Touch Interaction](./devlog/2025-12-18-014500_deck_builder_touch.md): Completed. Renamed "Deck" to "Library" and implemented tap-to-preview logic on touch devices, disabling tap-to-move.
- [Stack View Sorting & Sliders](./devlog/2025-12-18-020000_stack_sorting_sliders.md): Completed. Refactored StackView to group by Color by default, added sorting controls to Deck Builder, and reduced slider scales globally to allow smaller sizes. - [Stack View Sorting & Sliders](./devlog/2025-12-18-020000_stack_sorting_sliders.md): Completed. Refactored StackView to group by Color by default, added sorting controls to Deck Builder, and reduced slider scales globally to allow smaller sizes.
- [Lobby UI & Notifications](./devlog/2025-12-18-023000_lobby_ui_update.md): Completed. Refactored Lobby/Chat into collapsible floating panels, implemented player event notifications (Join/Leave/Disconnect), and updated Deck Builder card size triggers.
- [Card Preview Threshold](./devlog/2025-12-18-024000_preview_threshold.md): Completed. Updated card art crop threshold to 130px (new 50% mark) across the application components.

View File

@@ -0,0 +1,28 @@
# Work Plan - Lobby & Chat UI Overhaul and Notifications
## Request
1. **Lobby/Chat Sidebar**: Refactor to be collapsible on the right edge.
- Add floating/modal panel for Lobby and Chat content.
- Remove fixed right column layout.
2. **Notifications**: Implement toast notifications for player events (Join, Leave, Disconnect).
- Add setting to Enable/Disable notifications (persisted).
3. **Deck Builder**: Update "Full Card" display trigger to new slider range (50% = 130px).
## Changes
- **DeckBuilderView.tsx**:
- Updated `useArtCrop` logic: `cardWidth < 130` (was 200).
- **GameRoom.tsx**:
- Refactored layout: Added `activePanel` state.
- Created `useEffect` hook with `prevPlayersRef` to detect changes and trigger toasts.
- Added "Notifications On/Off" toggle in Lobby panel.
- Implemented floating side panel UI for desktop.
- Updated mobile view (kept separate mobile tab logic but ensured layout stability).
- **Toast.tsx**:
- Added `'warning'` type support for amber-colored alerts (Player Left).
## Verification
- Verified `ref` based diffing logic for notifications.
- Verified persistence of notification settings in `localStorage`.
- checked `Toast` type definition update.

View File

@@ -0,0 +1,12 @@
# Work Plan - Card Preview Threshold Update
## Request
- **Card Preview**: Change the trigger to show the full card to the what now is the new 50% (130px) instead of 200px.
## Changes
- **PackCard.tsx**: Updated logic to `cardWidth < 130` for art crop usage and `cardWidth >= 130` for hover preview prevention.
- **StackView.tsx**: Updated logic to `cardWidth < 130` and `cardWidth >= 130` respectively.
## Verification
- Verified code changes in `PackCard.tsx` and `StackView.tsx` via `replace_file_content` outputs.
- `DeckBuilderView.tsx` was already updated in previous step.

View File

@@ -104,11 +104,11 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
{viewMode === 'grid' && ( {viewMode === 'grid' && (
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{pack.cards.map((card) => { {pack.cards.map((card) => {
const useArtCrop = cardWidth < 200 && !!card.imageArtCrop; const useArtCrop = cardWidth < 130 && !!card.imageArtCrop;
const displayImage = useArtCrop ? card.imageArtCrop : card.image; const displayImage = useArtCrop ? card.imageArtCrop : card.image;
return ( return (
<CardHoverWrapper key={card.id} card={card} preventPreview={cardWidth >= 200}> <CardHoverWrapper key={card.id} card={card} preventPreview={cardWidth >= 130}>
<div style={{ width: cardWidth }} className="relative group bg-slate-900 rounded-lg shrink-0"> <div style={{ width: cardWidth }} className="relative group bg-slate-900 rounded-lg shrink-0">
{/* Visual Card */} {/* Visual Card */}
<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 cursor-pointer ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}> <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 cursor-pointer ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>

View File

@@ -125,7 +125,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
// Margin calculation: Negative margin to pull up next cards. // Margin calculation: Negative margin to pull up next cards.
// To show a "strip" of say 35px at the top of each card. // To show a "strip" of say 35px at the top of each card.
const isLast = index === catCards.length - 1; const isLast = index === catCards.length - 1;
const useArtCrop = cardWidth < 200 && !!card.imageArtCrop; const useArtCrop = cardWidth < 130 && !!card.imageArtCrop;
const displayImage = useArtCrop ? card.imageArtCrop : card.image; const displayImage = useArtCrop ? card.imageArtCrop : card.image;
return ( return (
@@ -163,7 +163,7 @@ const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHo
onTouchEnd={onTouchEnd} onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove} onTouchMove={onTouchMove}
> >
<CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 200}> <CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 130}>
<div <div
className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group-hover:ring-2 group-hover:ring-purple-400`} className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group-hover:ring-2 group-hover:ring-purple-400`}
style={{ style={{

View File

@@ -2,7 +2,7 @@
import React, { createContext, useContext, useState, useCallback } from 'react'; import React, { createContext, useContext, useState, useCallback } from 'react';
import { X, Check, AlertCircle, Info } from 'lucide-react'; import { X, Check, AlertCircle, Info } from 'lucide-react';
type ToastType = 'success' | 'error' | 'info'; type ToastType = 'success' | 'error' | 'info' | 'warning';
interface Toast { interface Toast {
id: string; id: string;
@@ -55,15 +55,18 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ childre
bg-slate-800 text-white bg-slate-800 text-white
${toast.type === 'success' ? 'border-emerald-500/50 shadow-emerald-900/20' : ${toast.type === 'success' ? 'border-emerald-500/50 shadow-emerald-900/20' :
toast.type === 'error' ? 'border-red-500/50 shadow-red-900/20' : toast.type === 'error' ? 'border-red-500/50 shadow-red-900/20' :
toast.type === 'warning' ? 'border-amber-500/50 shadow-amber-900/20' :
'border-blue-500/50 shadow-blue-900/20'} 'border-blue-500/50 shadow-blue-900/20'}
`} `}
> >
<div className={`p-2 rounded-full shrink-0 ${toast.type === 'success' ? 'bg-emerald-500/10 text-emerald-400' : <div className={`p-2 rounded-full shrink-0 ${toast.type === 'success' ? 'bg-emerald-500/10 text-emerald-400' :
toast.type === 'error' ? 'bg-red-500/10 text-red-400' : toast.type === 'error' ? 'bg-red-500/10 text-red-400' :
toast.type === 'warning' ? 'bg-amber-500/10 text-amber-400' :
'bg-blue-500/10 text-blue-400' 'bg-blue-500/10 text-blue-400'
}`}> }`}>
{toast.type === 'success' && <Check className="w-5 h-5" />} {toast.type === 'success' && <Check className="w-5 h-5" />}
{toast.type === 'error' && <AlertCircle className="w-5 h-5" />} {toast.type === 'error' && <AlertCircle className="w-5 h-5" />}
{toast.type === 'warning' && <AlertCircle className="w-5 h-5" />}
{toast.type === 'info' && <Info className="w-5 h-5" />} {toast.type === 'info' && <Info className="w-5 h-5" />}
</div> </div>

View File

@@ -191,7 +191,7 @@ const CardsDisplay: React.FC<{
> >
{cards.map(c => { {cards.map(c => {
const card = normalizeCard(c); const card = normalizeCard(c);
const useArtCrop = cardWidth < 200 && !!card.imageArtCrop; const useArtCrop = cardWidth < 130 && !!card.imageArtCrop;
const isFoil = card.finish === 'foil'; const isFoil = card.finish === 'foil';

View File

@@ -1,8 +1,9 @@
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 { Users, MessageSquare, Send, Copy, Check, Layers, LogOut } from 'lucide-react'; import { Users, MessageSquare, Send, Copy, Check, Layers, LogOut, Bell, BellOff, X } from 'lucide-react';
import { Modal } from '../../components/Modal'; import { Modal } from '../../components/Modal';
import { useToast } from '../../components/Toast';
import { GameView } from '../game/GameView'; import { GameView } from '../game/GameView';
import { DraftView } from '../draft/DraftView'; import { DraftView } from '../draft/DraftView';
import { DeckBuilderView } from '../draft/DeckBuilderView'; import { DeckBuilderView } from '../draft/DeckBuilderView';
@@ -45,18 +46,73 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [modalConfig, setModalConfig] = useState({ title: '', message: '', type: 'info' as 'info' | 'error' | 'warning' | 'success' }); const [modalConfig, setModalConfig] = useState({ title: '', message: '', type: 'info' as 'info' | 'error' | 'warning' | 'success' });
// Side Panel State
const [activePanel, setActivePanel] = useState<'lobby' | 'chat' | null>(null);
const [notificationsEnabled, setNotificationsEnabled] = useState(() => {
return localStorage.getItem('notifications_enabled') !== 'false';
});
// Services
const { showToast } = useToast();
// Restored States // Restored States
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [messages, setMessages] = useState<ChatMessage[]>(initialRoom.messages || []); const [messages, setMessages] = useState<ChatMessage[]>(initialRoom.messages || []);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [gameState, setGameState] = useState<any>(initialGameState || null); const [gameState, setGameState] = useState<any>(initialGameState || null);
const [draftState, setDraftState] = useState<any>(initialDraftState || null); const [draftState, setDraftState] = useState<any>(initialDraftState || null);
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile
// Derived State // Derived State
const host = room.players.find(p => p.isHost); const host = room.players.find(p => p.isHost);
const isHostOffline = host?.isOffline; const isHostOffline = host?.isOffline;
const isMeHost = currentPlayerId === host?.id; const isMeHost = currentPlayerId === host?.id;
const prevPlayersRef = useRef<Player[]>(initialRoom.players);
// Persistence
useEffect(() => {
localStorage.setItem('notifications_enabled', notificationsEnabled.toString());
}, [notificationsEnabled]);
// Player Notification Logic
useEffect(() => {
if (!notificationsEnabled) {
prevPlayersRef.current = room.players;
return;
}
const prev = prevPlayersRef.current;
const curr = room.players;
// 1. New Players
curr.forEach(p => {
if (!prev.find(old => old.id === p.id)) {
showToast(`${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');
}
});
// 3. Status Changes (Disconnect/Reconnect)
curr.forEach(p => {
const old = prev.find(o => o.id === p.id);
if (old) {
if (!old.isOffline && p.isOffline) {
showToast(`${p.name} lost connection.`, 'error');
}
if (old.isOffline && !p.isOffline) {
showToast(`${p.name} reconnected!`, 'success');
}
}
});
prevPlayersRef.current = curr;
}, [room.players, notificationsEnabled, showToast]);
// Effects // Effects
useEffect(() => { useEffect(() => {
@@ -235,9 +291,11 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
}; };
return ( return (
<div className="flex h-full flex-col lg:flex-row gap-4 overflow-hidden"> <div className="flex h-full w-full overflow-hidden relative">
{/* --- MOBILE LAYOUT (Keep simplified tabs for small screens) --- */}
<div className="lg:hidden flex flex-col w-full h-full">
{/* Mobile Tab Bar */} {/* Mobile Tab Bar */}
<div className="lg:hidden shrink-0 flex items-center bg-slate-800 border-b border-slate-700"> <div className="shrink-0 flex items-center bg-slate-800 border-b border-slate-700">
<button <button
onClick={() => setMobileTab('game')} 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'}`} 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'}`}
@@ -257,55 +315,134 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
</button> </button>
</div> </div>
<div className={`flex-1 min-h-0 flex flex-col ${mobileTab === 'game' ? 'flex' : 'hidden lg:flex'}`}> {/* Mobile Content */}
<div className="flex-1 min-h-0 relative">
{mobileTab === 'game' ? (
renderContent()
) : (
<div className="absolute inset-0 overflow-y-auto p-4 bg-slate-900">
{/* Mobile Chat/Lobby merged view for simplicity, reusing logic if possible or duplicating strictly for mobile structure */}
{/* Re-implementing simplified mobile view directly here to avoid layout conflicts */}
<div className="space-y-4">
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2"><Users className="w-4 h-4" /> Lobby</h3>
{room.players.map(p => (
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded mb-2 text-sm">
<span className={p.id === currentPlayerId ? 'text-white font-bold' : 'text-slate-300'}>{p.name}</span>
<span className="text-[10px] text-slate-500">{p.role}</span>
</div>
))}
</div>
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700 h-96 flex flex-col">
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3"><MessageSquare className="w-4 h-4 inline mr-2" /> Chat</h3>
<div className="flex-1 overflow-y-auto mb-2 space-y-2">
{messages.map(msg => (
<div key={msg.id} className="text-sm"><span className="font-bold text-purple-400">{msg.sender}:</span> <span className="text-slate-300">{msg.text}</span></div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="flex gap-2">
<input type="text" value={message} onChange={e => setMessage(e.target.value)} className="flex-1 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-sm text-white" placeholder="Type..." />
<button type="submit" className="bg-purple-600 rounded px-3 py-1 text-white"><Send className="w-4 h-4" /></button>
</form>
</div>
</div>
</div>
)}
</div>
</div>
{/* --- DESKTOP LAYOUT --- */}
{/* Main Content Area - Full Width */}
<div className="hidden lg:flex flex-1 min-w-0 flex-col h-full relative z-0">
{renderContent()} {renderContent()}
</div> </div>
<div className={`w-full lg:w-80 shrink-0 flex flex-col gap-4 min-h-0 ${mobileTab === 'chat' ? 'flex' : 'hidden lg:flex'}`}> {/* Right Collapsible Toolbar */}
<div className="flex-1 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl overflow-hidden flex flex-col"> <div className="hidden lg:flex w-14 shrink-0 flex-col items-center gap-4 py-4 bg-slate-900 border-l border-slate-800 z-30 relative">
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2"> <button
<Users className="w-4 h-4" /> Lobby onClick={() => setActivePanel(activePanel === 'lobby' ? null : 'lobby')}
className={`p-3 rounded-xl transition-all duration-200 group relative ${activePanel === 'lobby' ? 'bg-purple-600 text-white shadow-lg shadow-purple-900/50' : 'text-slate-500 hover:text-purple-400 hover:bg-slate-800'}`}
title="Lobby & Players"
>
<Users className="w-6 h-6" />
<span className="absolute right-full mr-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">
Lobby
</span>
</button>
<button
onClick={() => setActivePanel(activePanel === 'chat' ? null : 'chat')}
className={`p-3 rounded-xl transition-all duration-200 group relative ${activePanel === 'chat' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' : 'text-slate-500 hover:text-blue-400 hover:bg-slate-800'}`}
title="Chat"
>
<div className="relative">
<MessageSquare className="w-6 h-6" />
{/* Unread indicator could go here */}
</div>
<span className="absolute right-full mr-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">
Chat
</span>
</button>
</div>
{/* Floating Panel (Desktop) */}
{activePanel && (
<div className="hidden lg:flex absolute right-16 top-4 bottom-4 w-96 bg-slate-800/95 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl z-40 flex-col animate-in slide-in-from-right-10 fade-in duration-200 overflow-hidden ring-1 ring-white/10">
{/* Header */}
<div className="p-4 border-b border-slate-700 flex justify-between items-center bg-slate-900/50">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
{activePanel === 'lobby' ? <><Users className="w-5 h-5 text-purple-400" /> Lobby</> : <><MessageSquare className="w-5 h-5 text-blue-400" /> Chat</>}
</h3> </h3>
<button onClick={() => setActivePanel(null)} className="p-1 hover:bg-slate-700 rounded-lg text-slate-400 hover:text-white transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* Lobby Content */}
{activePanel === 'lobby' && (
<div className="flex-1 flex flex-col min-h-0">
{/* Controls */}
<div className="p-3 bg-slate-900/30 flex items-center justify-between border-b border-slate-800">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">{room.players.length} Connected</span>
<button
onClick={() => setNotificationsEnabled(!notificationsEnabled)}
className={`flex items-center gap-2 text-xs font-bold px-2 py-1 rounded-lg transition-colors border ${notificationsEnabled ? 'bg-slate-800 border-slate-600 text-slate-300 hover:text-white' : 'bg-red-900/20 border-red-900/50 text-red-400'}`}
title={notificationsEnabled ? "Disable Notifications" : "Enable Notifications"}
>
{notificationsEnabled ? <Bell className="w-3 h-3" /> : <BellOff className="w-3 h-3" />}
{notificationsEnabled ? 'On' : 'Off'}
</button>
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1"> {/* Player List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar">
{room.players.map(p => { {room.players.map(p => {
const isReady = (p as any).ready; const isReady = (p as any).ready;
const isMe = p.id === currentPlayerId; const isMe = p.id === currentPlayerId;
const isSolo = room.players.length === 1 && room.status === 'playing'; const isSolo = room.players.length === 1 && room.status === 'playing';
return ( return (
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded-lg border border-slate-700/50 group"> <div key={p.id} className="flex items-center justify-between bg-slate-900/80 p-3 rounded-xl border border-slate-700/50 hover:border-slate-600 transition-colors group">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs ${p.role === 'spectator' ? 'bg-slate-700 text-slate-300' : 'bg-gradient-to-br from-purple-500 to-blue-500 text-white'}`}> <div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm shadow-inner ${p.role === 'spectator' ? 'bg-slate-800 text-slate-500' : 'bg-gradient-to-br from-purple-600 to-blue-600 text-white shadow-purple-900/30'}`}>
{p.name.substring(0, 2).toUpperCase()} {p.name.substring(0, 2).toUpperCase()}
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className={`text-sm font-medium ${isMe ? 'text-white' : 'text-slate-300'}`}> <span className={`text-sm font-bold ${isMe ? 'text-white' : 'text-slate-200'}`}>
{p.name} {isMe && '(You)'} {p.name} {isMe && <span className="text-slate-500 font-normal">(You)</span>}
</span> </span>
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500"> <span className="text-[10px] uppercase font-bold tracking-wider text-slate-500 flex items-center gap-1">
{p.role} {p.isHost && <span className="text-amber-500 ml-1"> Host</span>} {p.role}
{isReady && room.status === 'deck_building' && <span className="text-emerald-500 ml-1"> Ready</span>} {p.isHost && <span className="text-amber-500 flex items-center"> Host</span>}
{p.isOffline && <span className="text-red-500 ml-1"> Offline</span>} {isReady && room.status === 'deck_building' && <span className="text-emerald-500 flex items-center"> Ready</span>}
{p.isOffline && <span className="text-red-500 flex items-center"> Offline</span>}
</span> </span>
</div> </div>
</div> </div>
<div className={`flex gap-2 ${isSolo ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}> <div className={`flex gap-1 ${isSolo ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}>
{isMe && (
<button
onClick={onExit}
className={`p-1 rounded flex items-center gap-2 transition-colors ${isSolo
? 'bg-red-900/40 text-red-200 hover:bg-red-900/60 px-3 py-1.5'
: 'hover:bg-slate-700 text-slate-400 hover:text-red-400'
}`}
title={isSolo ? "End Solo Session" : "Leave Room"}
>
<LogOut className="w-4 h-4" />
{isSolo && <span className="text-xs font-bold">End Test</span>}
</button>
)}
{isMeHost && !isMe && ( {isMeHost && !isMe && (
<button <button
onClick={() => { onClick={() => {
@@ -313,46 +450,65 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
socketService.socket.emit('kick_player', { roomId: room.id, targetId: p.id }); socketService.socket.emit('kick_player', { roomId: room.id, targetId: p.id });
} }
}} }}
className="p-1 hover:bg-red-900/50 rounded text-slate-500 hover:text-red-500" className="p-1.5 hover:bg-red-500/10 rounded-lg text-slate-500 hover:text-red-500 transition-colors"
title="Kick Player" title="Kick Player"
> >
<LogOut className="w-4 h-4 rotate-180" /> <LogOut className="w-4 h-4 rotate-180" />
</button> </button>
)} )}
{isMe && (
<button onClick={onExit} className="p-1.5 hover:bg-red-500/10 rounded-lg text-slate-400 hover:text-red-400 transition-colors" title="Accions">
<LogOut className="w-4 h-4" />
</button>
)}
</div> </div>
</div> </div>
) )
})} })}
</div> </div>
</div> </div>
)}
<div className="h-1/2 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl flex flex-col"> {/* Chat Content */}
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2"> {activePanel === 'chat' && (
<MessageSquare className="w-4 h-4" /> Chat <div className="flex-1 flex flex-col min-h-0">
</h3> <div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
<div className="flex-1 overflow-y-auto space-y-2 mb-3 pr-1 custom-scrollbar"> {messages.length === 0 && (
<div className="text-center text-slate-600 mt-10 text-sm italic">
No messages yet. Say hello!
</div>
)}
{messages.map(msg => ( {messages.map(msg => (
<div key={msg.id} className="text-sm"> <div key={msg.id} className={`flex flex-col ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'items-end' : 'items-start'}`}>
<span className="font-bold text-purple-400 text-xs">{msg.sender}: </span> <div className={`max-w-[85%] px-3 py-2 rounded-xl text-sm ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'bg-blue-600 text-white rounded-br-none shadow-blue-900/20' : 'bg-slate-700 text-slate-200 rounded-bl-none'}`}>
<span className="text-slate-300">{msg.text}</span> {msg.text}
</div>
<span className="text-[10px] text-slate-500 mt-1 font-medium">{msg.sender}</span>
</div> </div>
))} ))}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
<div className="p-3 bg-slate-900/50 border-t border-slate-700">
<form onSubmit={sendMessage} className="flex gap-2"> <form onSubmit={sendMessage} className="flex gap-2">
<input <input
type="text" type="text"
value={message} value={message}
onChange={e => setMessage(e.target.value)} onChange={e => setMessage(e.target.value)}
className="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-purple-500" className="flex-1 bg-slate-950 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
placeholder="Type..." placeholder="Type a message..."
/> />
<button type="submit" className="p-2 bg-purple-600 hover:bg-purple-500 rounded-lg text-white transition-colors"> <button type="submit" className="p-2.5 bg-blue-600 hover:bg-blue-500 rounded-xl text-white transition-all shadow-lg shadow-blue-900/20 disabled:opacity-50" disabled={!message.trim()}>
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
</button> </button>
</form> </form>
</div> </div>
</div> </div>
)}
</div>
)}
{/* Host Disconnected Overlay */} {/* Host Disconnected Overlay */}
{isHostOffline && !isMeHost && ( {isHostOffline && !isMeHost && (