From ebfdfef5aeb1869c1ff396bca2038e75dec72d82 Mon Sep 17 00:00:00 2001 From: dnviti Date: Thu, 18 Dec 2025 01:38:28 +0100 Subject: [PATCH] feat: refactor lobby UI with collapsible panels, add player event notifications, and update card art crop threshold to 130px --- docs/development/CENTRAL.md | 2 + .../2025-12-18-023000_lobby_ui_update.md | 28 ++ .../2025-12-18-024000_preview_threshold.md | 12 + src/client/src/components/PackCard.tsx | 4 +- src/client/src/components/StackView.tsx | 4 +- src/client/src/components/Toast.tsx | 9 +- .../src/modules/draft/DeckBuilderView.tsx | 2 +- src/client/src/modules/lobby/GameRoom.tsx | 380 ++++++++++++------ 8 files changed, 321 insertions(+), 120 deletions(-) create mode 100644 docs/development/devlog/2025-12-18-023000_lobby_ui_update.md create mode 100644 docs/development/devlog/2025-12-18-024000_preview_threshold.md diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 7fa529c..49a9091 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -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. - [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. +- [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. diff --git a/docs/development/devlog/2025-12-18-023000_lobby_ui_update.md b/docs/development/devlog/2025-12-18-023000_lobby_ui_update.md new file mode 100644 index 0000000..46dbca0 --- /dev/null +++ b/docs/development/devlog/2025-12-18-023000_lobby_ui_update.md @@ -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. diff --git a/docs/development/devlog/2025-12-18-024000_preview_threshold.md b/docs/development/devlog/2025-12-18-024000_preview_threshold.md new file mode 100644 index 0000000..94c8f5c --- /dev/null +++ b/docs/development/devlog/2025-12-18-024000_preview_threshold.md @@ -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. diff --git a/src/client/src/components/PackCard.tsx b/src/client/src/components/PackCard.tsx index e78ed5c..8aa34c1 100644 --- a/src/client/src/components/PackCard.tsx +++ b/src/client/src/components/PackCard.tsx @@ -104,11 +104,11 @@ export const PackCard: React.FC = ({ pack, viewMode, cardWidth = {viewMode === 'grid' && (
{pack.cards.map((card) => { - const useArtCrop = cardWidth < 200 && !!card.imageArtCrop; + const useArtCrop = cardWidth < 130 && !!card.imageArtCrop; const displayImage = useArtCrop ? card.imageArtCrop : card.image; return ( - = 200}> + = 130}>
{/* Visual Card */}
diff --git a/src/client/src/components/StackView.tsx b/src/client/src/components/StackView.tsx index 8c2c471..d8d8530 100644 --- a/src/client/src/components/StackView.tsx +++ b/src/client/src/components/StackView.tsx @@ -125,7 +125,7 @@ export const StackView: React.FC = ({ cards, cardWidth = 150, on // Margin calculation: Negative margin to pull up next cards. // To show a "strip" of say 35px at the top of each card. 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; return ( @@ -163,7 +163,7 @@ const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHo onTouchEnd={onTouchEnd} onTouchMove={onTouchMove} > - = 200}> + = 130}>
= ({ childre bg-slate-800 text-white ${toast.type === 'success' ? 'border-emerald-500/50 shadow-emerald-900/20' : toast.type === 'error' ? 'border-red-500/50 shadow-red-900/20' : - 'border-blue-500/50 shadow-blue-900/20'} + toast.type === 'warning' ? 'border-amber-500/50 shadow-amber-900/20' : + 'border-blue-500/50 shadow-blue-900/20'} `} >
{toast.type === 'success' && } {toast.type === 'error' && } + {toast.type === 'warning' && } {toast.type === 'info' && }
diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index 1785b6e..7a2f21a 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -191,7 +191,7 @@ const CardsDisplay: React.FC<{ > {cards.map(c => { const card = normalizeCard(c); - const useArtCrop = cardWidth < 200 && !!card.imageArtCrop; + const useArtCrop = cardWidth < 130 && !!card.imageArtCrop; const isFoil = card.finish === 'foil'; diff --git a/src/client/src/modules/lobby/GameRoom.tsx b/src/client/src/modules/lobby/GameRoom.tsx index 4cebef7..17debe1 100644 --- a/src/client/src/modules/lobby/GameRoom.tsx +++ b/src/client/src/modules/lobby/GameRoom.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useRef } from 'react'; 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 { useToast } from '../../components/Toast'; import { GameView } from '../game/GameView'; import { DraftView } from '../draft/DraftView'; import { DeckBuilderView } from '../draft/DeckBuilderView'; @@ -45,18 +46,73 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl const [modalOpen, setModalOpen] = useState(false); 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 const [message, setMessage] = useState(''); const [messages, setMessages] = useState(initialRoom.messages || []); const messagesEndRef = useRef(null); const [gameState, setGameState] = useState(initialGameState || null); const [draftState, setDraftState] = useState(initialDraftState || null); - const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); + const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile // Derived State const host = room.players.find(p => p.isHost); const isHostOffline = host?.isOffline; const isMeHost = currentPlayerId === host?.id; + const prevPlayersRef = useRef(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 useEffect(() => { @@ -235,125 +291,225 @@ export const GameRoom: React.FC = ({ room: initialRoom, currentPl }; return ( -
- {/* Mobile Tab Bar */} -
- - +
+ {/* --- MOBILE LAYOUT (Keep simplified tabs for small screens) --- */} +
+ {/* Mobile Tab Bar */} +
+ + +
+ + {/* Mobile Content */} +
+ {mobileTab === 'game' ? ( + renderContent() + ) : ( +
+ {/* 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 */} +
+
+

Lobby

+ {room.players.map(p => ( +
+ {p.name} + {p.role} +
+ ))} +
+
+

Chat

+
+ {messages.map(msg => ( +
{msg.sender}: {msg.text}
+ ))} +
+
+
+ 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..." /> + +
+
+
+
+ )} +
-
+ {/* --- DESKTOP LAYOUT --- */} + {/* Main Content Area - Full Width */} +
{renderContent()}
-
-
-

- Lobby -

+ {/* Right Collapsible Toolbar */} +
+ - -
- {room.players.map(p => { - const isReady = (p as any).ready; - const isMe = p.id === currentPlayerId; - const isSolo = room.players.length === 1 && room.status === 'playing'; - - return ( -
-
-
- {p.name.substring(0, 2).toUpperCase()} -
-
- - {p.name} {isMe && '(You)'} - - - {p.role} {p.isHost && • Host} - {isReady && room.status === 'deck_building' && • Ready} - {p.isOffline && • Offline} - -
-
- -
- {isMe && ( - - )} - {isMeHost && !isMe && ( - - )} -
-
- ) - })} +
- -
-

- Chat -

-
- {messages.map(msg => ( -
- {msg.sender}: - {msg.text} -
- ))} -
-
-
- 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" - placeholder="Type..." - /> - -
-
+ + Chat + +
+ {/* Floating Panel (Desktop) */} + {activePanel && ( +
+ + {/* Header */} +
+

+ {activePanel === 'lobby' ? <> Lobby : <> Chat} +

+ +
+ + {/* Lobby Content */} + {activePanel === 'lobby' && ( +
+ {/* Controls */} +
+ {room.players.length} Connected + +
+ + {/* Player List */} +
+ {room.players.map(p => { + const isReady = (p as any).ready; + const isMe = p.id === currentPlayerId; + const isSolo = room.players.length === 1 && room.status === 'playing'; + + return ( +
+
+
+ {p.name.substring(0, 2).toUpperCase()} +
+
+ + {p.name} {isMe && (You)} + + + {p.role} + {p.isHost && • Host} + {isReady && room.status === 'deck_building' && • Ready} + {p.isOffline && • Offline} + +
+
+ +
+ {isMeHost && !isMe && ( + + )} + {isMe && ( + + )} +
+
+ ) + })} +
+
+ )} + + {/* Chat Content */} + {activePanel === 'chat' && ( +
+
+ {messages.length === 0 && ( +
+ No messages yet. Say hello! +
+ )} + {messages.map(msg => ( +
p.id === currentPlayerId)?.name) ? 'items-end' : 'items-start'}`}> +
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'}`}> + {msg.text} +
+ {msg.sender} +
+ ))} +
+
+
+
+ setMessage(e.target.value)} + 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 a message..." + /> + +
+
+
+ )} + +
+ )} + + + {/* Host Disconnected Overlay */} {isHostOffline && !isMeHost && (