813 lines
25 KiB
TypeScript
813 lines
25 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react'
|
||
import { useParams, useNavigate } from 'react-router-dom'
|
||
import {
|
||
Container,
|
||
Typography,
|
||
Box,
|
||
Card,
|
||
CardContent,
|
||
TextField,
|
||
Button,
|
||
Alert,
|
||
CircularProgress,
|
||
Chip,
|
||
List,
|
||
ListItem,
|
||
ListItemText,
|
||
ListItemAvatar,
|
||
Avatar,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Snackbar,
|
||
Divider,
|
||
Tooltip,
|
||
Stack,
|
||
IconButton,
|
||
} from '@mui/material'
|
||
import {
|
||
ContentCopy,
|
||
DragIndicator,
|
||
Delete,
|
||
Share,
|
||
PlayArrow,
|
||
Stop,
|
||
Lock,
|
||
LockOpen,
|
||
SkipNext,
|
||
KeyboardDoubleArrowRight,
|
||
} from '@mui/icons-material'
|
||
import {
|
||
DndContext,
|
||
closestCenter,
|
||
DragEndEvent,
|
||
useSensor,
|
||
useSensors,
|
||
PointerSensor,
|
||
KeyboardSensor
|
||
} from '@dnd-kit/core'
|
||
import {
|
||
SortableContext,
|
||
verticalListSortingStrategy,
|
||
useSortable,
|
||
arrayMove,
|
||
sortableKeyboardCoordinates
|
||
} from '@dnd-kit/sortable'
|
||
import { CSS } from '@dnd-kit/utilities'
|
||
import axios from 'axios'
|
||
import { useAuth } from '../../contexts/AuthContext'
|
||
import AppNavigation from '../AppNavigation'
|
||
|
||
interface PlayerData {
|
||
username: string
|
||
left?: boolean
|
||
}
|
||
|
||
interface Lobby {
|
||
id: string
|
||
name: string
|
||
description?: string
|
||
owner: string
|
||
players: PlayerData[]
|
||
turn: number
|
||
inviteCode: string
|
||
password?: string
|
||
locked?: boolean
|
||
}
|
||
|
||
interface SortablePlayerProps {
|
||
player: PlayerData
|
||
index: number
|
||
isCurrentTurn: boolean
|
||
isOwner: boolean
|
||
canManage: boolean
|
||
locked: boolean
|
||
onRemove: (username: string) => void
|
||
onSetCurrentTurn: (username: string) => void
|
||
}
|
||
|
||
const SortablePlayer: React.FC<SortablePlayerProps> = ({
|
||
player,
|
||
index,
|
||
isCurrentTurn,
|
||
isOwner,
|
||
canManage,
|
||
locked,
|
||
onRemove,
|
||
onSetCurrentTurn
|
||
}) => {
|
||
const {
|
||
attributes,
|
||
listeners,
|
||
setNodeRef,
|
||
transform,
|
||
transition,
|
||
} = useSortable({ id: player.username, disabled: locked })
|
||
|
||
const style = {
|
||
transform: CSS.Transform.toString(transform),
|
||
transition,
|
||
}
|
||
|
||
return (
|
||
<ListItem
|
||
ref={setNodeRef}
|
||
style={style}
|
||
sx={{
|
||
mb: 1,
|
||
bgcolor: isCurrentTurn ? 'primary.light' : 'background.paper',
|
||
borderRadius: 2,
|
||
boxShadow: isCurrentTurn ? 2 : 1,
|
||
border: isCurrentTurn ? '2px solid' : '1px solid',
|
||
borderColor: isCurrentTurn ? 'primary.main' : 'divider',
|
||
opacity: player.left ? 0.5 : 1,
|
||
}}
|
||
secondaryAction={
|
||
canManage && !locked && !player.left && (
|
||
<Box display="flex" gap={1}>
|
||
{!isCurrentTurn && (
|
||
<Tooltip title="Set as current turn">
|
||
<IconButton
|
||
onClick={() => onSetCurrentTurn(player.username)}
|
||
color="primary"
|
||
size="small"
|
||
>
|
||
<KeyboardDoubleArrowRight />
|
||
</IconButton>
|
||
</Tooltip>
|
||
)}
|
||
<Tooltip title="Remove player">
|
||
<IconButton
|
||
onClick={() => onRemove(player.username)}
|
||
color="error"
|
||
size="small"
|
||
>
|
||
<Delete />
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Box>
|
||
)
|
||
}
|
||
>
|
||
{canManage && (
|
||
<ListItemAvatar
|
||
{...(!locked ? attributes : {})}
|
||
{...(!locked ? listeners : {})}
|
||
sx={{ cursor: locked ? 'default' : 'grab' }}
|
||
>
|
||
<Avatar sx={{
|
||
bgcolor: isCurrentTurn ? 'primary.main' : 'grey.400',
|
||
opacity: locked ? 0.5 : 1
|
||
}}>
|
||
<DragIndicator />
|
||
</Avatar>
|
||
</ListItemAvatar>
|
||
)}
|
||
|
||
<ListItemText
|
||
primary={
|
||
<Box display="flex" alignItems="center" gap={1}>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
textDecoration: player.left ? 'line-through' : 'none',
|
||
color: isCurrentTurn ? 'primary.contrastText' : 'text.primary',
|
||
}}
|
||
>
|
||
{player.username}
|
||
</Typography>
|
||
{isOwner && <Chip label="Owner" size="small" color="secondary" />}
|
||
{isCurrentTurn && <Chip label="Current Turn" size="small" color="primary" />}
|
||
{player.left && <Chip label="Left" size="small" variant="outlined" />}
|
||
</Box>
|
||
}
|
||
secondary={isCurrentTurn ? "It's your turn!" : `Position ${index + 1}`}
|
||
sx={{
|
||
'& .MuiListItemText-secondary': {
|
||
color: isCurrentTurn ? 'primary.contrastText' : 'text.secondary',
|
||
}
|
||
}}
|
||
/>
|
||
</ListItem>
|
||
)
|
||
}
|
||
|
||
const LobbyPage: React.FC = () => {
|
||
const { user, notificationsEnabled } = useAuth()
|
||
const { id } = useParams<{ id: string }>()
|
||
const navigate = useNavigate()
|
||
|
||
// State management
|
||
const [lobby, setLobby] = useState<Lobby | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState('')
|
||
const [snackbar, setSnackbar] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||
open: false,
|
||
message: '',
|
||
severity: 'success'
|
||
})
|
||
|
||
// Settings
|
||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||
const [newGameName, setNewGameName] = useState('')
|
||
const [newDescription, setNewDescription] = useState('')
|
||
const [newPassword, setNewPassword] = useState('')
|
||
|
||
// Leave confirmation
|
||
const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false)
|
||
|
||
// Invite confirmation
|
||
const [inviteConfirmOpen, setInviteConfirmOpen] = useState(false)
|
||
const [inviteCode, setInviteCode] = useState<string | null>(null)
|
||
|
||
// Drag and drop
|
||
const sensors = useSensors(
|
||
useSensor(PointerSensor),
|
||
useSensor(KeyboardSensor, {
|
||
coordinateGetter: sortableKeyboardCoordinates,
|
||
})
|
||
)
|
||
|
||
const fetchLobby = useCallback(async () => {
|
||
try {
|
||
const response = await axios.get(`http://localhost:3001/api/lobbies/${id}`)
|
||
setLobby(response.data)
|
||
setNewGameName(response.data.name)
|
||
setNewDescription(response.data.description || '')
|
||
|
||
// Check for pending invite after loading lobby
|
||
const pendingInvite = localStorage.getItem('pendingInvite')
|
||
if (pendingInvite && response.data.inviteCode === pendingInvite) {
|
||
// Check if user is already in the lobby
|
||
const userInLobby = response.data.players.some((p: any) => p.username === user?.username && !p.left)
|
||
if (!userInLobby) {
|
||
setInviteCode(pendingInvite)
|
||
setInviteConfirmOpen(true)
|
||
}
|
||
localStorage.removeItem('pendingInvite')
|
||
}
|
||
} catch (error) {
|
||
setError('Failed to load lobby')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [id, user?.username])
|
||
|
||
useEffect(() => {
|
||
if (!user) {
|
||
navigate('/')
|
||
return
|
||
}
|
||
|
||
fetchLobby()
|
||
|
||
// WebSocket connection
|
||
const ws = new WebSocket('ws://localhost:3001')
|
||
ws.onmessage = (event) => {
|
||
const message = JSON.parse(event.data)
|
||
if (message.lobby?.id === id) {
|
||
setLobby(message.lobby)
|
||
setNewGameName(message.lobby.name)
|
||
|
||
// Show notification for turn changes
|
||
if (message.type === 'turn-change' && message.lobby.players[message.lobby.turn]?.username === user?.username) {
|
||
showSnackbar("It's your turn!", 'success')
|
||
if (notificationsEnabled && Notification.permission === 'granted') {
|
||
new Notification("Your Turn!", {
|
||
body: `It's your turn in ${message.lobby.name}`,
|
||
icon: '/logo192.png',
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Polling backup every 5 minutes
|
||
const interval = setInterval(fetchLobby, 5 * 60 * 1000)
|
||
|
||
return () => {
|
||
ws.close()
|
||
clearInterval(interval)
|
||
}
|
||
}, [id, user, navigate, notificationsEnabled, fetchLobby])
|
||
|
||
const showSnackbar = (message: string, severity: 'success' | 'error' = 'success') => {
|
||
setSnackbar({ open: true, message, severity })
|
||
}
|
||
|
||
|
||
|
||
const handleEndTurn = async () => {
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/end-turn`, {
|
||
username: user?.username
|
||
})
|
||
showSnackbar('Turn advanced!')
|
||
} catch (error) {
|
||
showSnackbar('Failed to end turn', 'error')
|
||
}
|
||
}
|
||
|
||
const handleLockToggle = async () => {
|
||
if (!lobby || !user || lobby.owner !== user.username) return
|
||
|
||
try {
|
||
const endpoint = lobby.locked ? 'unlock' : 'lock'
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/${endpoint}`, {
|
||
username: user.username
|
||
})
|
||
const action = lobby.locked ? 'unlocked' : 'locked'
|
||
showSnackbar(`Turn order ${action}!`, 'success')
|
||
fetchLobby()
|
||
} catch (error) {
|
||
showSnackbar('Failed to change lock state', 'error')
|
||
}
|
||
}
|
||
|
||
const handleLeaveLobby = () => {
|
||
setLeaveConfirmOpen(true)
|
||
}
|
||
|
||
const confirmLeaveLobby = async () => {
|
||
if (!user) return
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/leave`, { username: user.username })
|
||
showSnackbar('Left lobby')
|
||
navigate('/')
|
||
} catch (error) {
|
||
showSnackbar('Failed to leave lobby', 'error')
|
||
}
|
||
setLeaveConfirmOpen(false)
|
||
}
|
||
|
||
const handleJoinFromInvite = async (password?: string) => {
|
||
if (!user || !inviteCode) return
|
||
|
||
try {
|
||
await axios.post('http://localhost:3001/api/lobbies/join', {
|
||
inviteCode,
|
||
username: user.username,
|
||
password,
|
||
})
|
||
setInviteConfirmOpen(false)
|
||
setInviteCode(null)
|
||
showSnackbar('Successfully joined the lobby!', 'success')
|
||
fetchLobby() // Refresh lobby data
|
||
} catch (error: any) {
|
||
if (error.response?.status === 401) {
|
||
showSnackbar('Incorrect password. Please try again.', 'error')
|
||
} else if (error.response?.status === 400) {
|
||
if (error.response.data.includes('full')) {
|
||
showSnackbar('This lobby is full (maximum 10 players).', 'error')
|
||
setInviteConfirmOpen(false)
|
||
setInviteCode(null)
|
||
} else {
|
||
showSnackbar('Failed to join lobby. Please try again.', 'error')
|
||
}
|
||
} else {
|
||
showSnackbar('Failed to join lobby. Please try again.', 'error')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleDeclineInvite = () => {
|
||
setInviteConfirmOpen(false)
|
||
setInviteCode(null)
|
||
navigate('/')
|
||
}
|
||
|
||
const handleRemovePlayer = async (username: string) => {
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/remove`, { username, removedBy: user?.username })
|
||
showSnackbar(`Removed ${username} from lobby`)
|
||
} catch (error) {
|
||
showSnackbar('Failed to remove player', 'error')
|
||
}
|
||
}
|
||
|
||
const handleSetCurrentTurn = async (username: string) => {
|
||
if (!user) return
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/set-current-turn`, {
|
||
targetUsername: username,
|
||
username: user.username
|
||
})
|
||
showSnackbar(`Turn set to ${username}`)
|
||
} catch (error) {
|
||
showSnackbar('Failed to set current turn', 'error')
|
||
}
|
||
}
|
||
|
||
const handleSetPassword = async () => {
|
||
if (!user) return
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/set-password`, {
|
||
password: newPassword,
|
||
username: user.username
|
||
})
|
||
showSnackbar('Lobby password updated!')
|
||
setNewPassword('')
|
||
} catch (error) {
|
||
showSnackbar('Failed to set password', 'error')
|
||
}
|
||
}
|
||
|
||
const handleSaveSettings = async () => {
|
||
if (!user) return
|
||
try {
|
||
// Update name if changed
|
||
if (newGameName !== lobby?.name) {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/rename`, {
|
||
newName: newGameName,
|
||
username: user.username
|
||
})
|
||
}
|
||
|
||
// Update description if changed
|
||
if (newDescription !== (lobby?.description || '')) {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/set-description`, {
|
||
description: newDescription,
|
||
username: user.username
|
||
})
|
||
}
|
||
|
||
showSnackbar('Settings saved successfully!')
|
||
setSettingsOpen(false)
|
||
fetchLobby() // Refresh lobby data
|
||
} catch (error) {
|
||
showSnackbar('Failed to save settings', 'error')
|
||
}
|
||
}
|
||
|
||
const handleDragEnd = async (event: DragEndEvent) => {
|
||
if (!lobby || lobby.locked) return
|
||
|
||
const { active, over } = event
|
||
|
||
if (over && active.id !== over.id) {
|
||
const oldIndex = lobby.players.findIndex(p => p.username === active.id)
|
||
const newIndex = lobby.players.findIndex(p => p.username === over.id)
|
||
const newPlayers = arrayMove(lobby.players, oldIndex, newIndex)
|
||
|
||
try {
|
||
await axios.post(`http://localhost:3001/api/lobbies/${id}/reorder`, {
|
||
players: newPlayers,
|
||
username: user?.username
|
||
})
|
||
showSnackbar('Player order updated!')
|
||
} catch (error) {
|
||
showSnackbar('Failed to reorder players', 'error')
|
||
}
|
||
}
|
||
}
|
||
|
||
const copyInviteLink = () => {
|
||
if (!lobby) return
|
||
const inviteLink = `${window.location.origin}/join/${lobby.inviteCode}`
|
||
navigator.clipboard.writeText(inviteLink)
|
||
showSnackbar('Invite link copied to clipboard!')
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">
|
||
<CircularProgress size={60} />
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
if (error || !lobby || !user) {
|
||
return (
|
||
<Container maxWidth="md" sx={{ py: 4 }}>
|
||
<Alert severity="error">
|
||
{error || 'Lobby not found'}
|
||
</Alert>
|
||
<Button onClick={() => navigate('/')} sx={{ mt: 2 }}>
|
||
Go Home
|
||
</Button>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
const isOwner = user?.username === lobby.owner
|
||
const currentPlayer = lobby.players[lobby.turn]
|
||
const isCurrentTurn = currentPlayer?.username === user?.username
|
||
|
||
return (
|
||
<>
|
||
<AppNavigation />
|
||
|
||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||
{/* Lobby Header */}
|
||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||
<Typography variant="h2" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||
{lobby?.name}
|
||
</Typography>
|
||
{lobby?.description && (
|
||
<Typography variant="h6" color="text.secondary" sx={{ maxWidth: '800px', mx: 'auto' }}>
|
||
{lobby.description}
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: { xs: 'column', md: 'row' },
|
||
gap: 4,
|
||
}}
|
||
>
|
||
<Box sx={{ flex: { xs: 1, md: '2 1 0%' } }}>
|
||
<Card elevation={3}>
|
||
<CardContent>
|
||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||
<Typography variant="h4" gutterBottom>
|
||
Turn Order
|
||
</Typography>
|
||
<Box>
|
||
<Tooltip title="Copy invite link">
|
||
<IconButton onClick={copyInviteLink} color="primary">
|
||
<Share />
|
||
</IconButton>
|
||
</Tooltip>
|
||
{isOwner && (
|
||
<Tooltip title={lobby.locked ? 'Unlock turn order' : 'Lock turn order'}>
|
||
<IconButton onClick={handleLockToggle} color={lobby.locked ? 'warning' : 'default'}>
|
||
{lobby.locked ? <Lock /> : <LockOpen />}
|
||
</IconButton>
|
||
</Tooltip>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
|
||
{lobby.locked && (
|
||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||
<Box display="flex" alignItems="center" gap={1}>
|
||
<Lock fontSize="small" />
|
||
Turn order is locked and cannot be changed
|
||
</Box>
|
||
</Alert>
|
||
)}
|
||
|
||
{lobby.players.length === 0 ? (
|
||
<Alert severity="info">No players in this lobby yet.</Alert>
|
||
) : (
|
||
<DndContext
|
||
sensors={sensors}
|
||
collisionDetection={closestCenter}
|
||
onDragEnd={handleDragEnd}
|
||
>
|
||
<SortableContext
|
||
items={lobby.players.map(p => p.username)}
|
||
strategy={verticalListSortingStrategy}
|
||
>
|
||
<List>
|
||
{lobby.players.map((player, index) => (
|
||
<SortablePlayer
|
||
key={player.username}
|
||
player={player}
|
||
index={index}
|
||
isCurrentTurn={lobby.turn === index}
|
||
isOwner={player.username === lobby.owner}
|
||
canManage={isOwner}
|
||
locked={lobby.locked || false}
|
||
onRemove={handleRemovePlayer}
|
||
onSetCurrentTurn={handleSetCurrentTurn}
|
||
/>
|
||
))}
|
||
</List>
|
||
</SortableContext>
|
||
</DndContext>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Box sx={{ flex: { xs: 1, md: '1 1 0%' } }}>
|
||
<Stack spacing={3}>
|
||
<Card elevation={3}>
|
||
<CardContent>
|
||
<Typography variant="h5" gutterBottom>
|
||
Game Actions
|
||
</Typography>
|
||
<Divider sx={{ mb: 2 }} />
|
||
|
||
{isCurrentTurn && (
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
size="large"
|
||
startIcon={<PlayArrow />}
|
||
onClick={handleEndTurn}
|
||
sx={{ mb: 2 }}
|
||
>
|
||
End My Turn
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
fullWidth
|
||
variant="outlined"
|
||
color="error"
|
||
startIcon={<Stop />}
|
||
onClick={handleLeaveLobby}
|
||
>
|
||
Leave Lobby
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card elevation={3}>
|
||
<CardContent>
|
||
<Typography variant="h6" gutterBottom>
|
||
Lobby Info
|
||
</Typography>
|
||
<Divider sx={{ mb: 2 }} />
|
||
|
||
<Box mb={2}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Players: {lobby.players.filter(p => !p.left).length}/{lobby.players.length}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Current Turn: {currentPlayer?.username || 'Unknown'}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Owner: {lobby.owner}
|
||
</Typography>
|
||
</Box>
|
||
|
||
<Button
|
||
fullWidth
|
||
variant="outlined"
|
||
startIcon={<ContentCopy />}
|
||
onClick={copyInviteLink}
|
||
>
|
||
Copy Invite Link
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</Stack>
|
||
</Box>
|
||
</Box>
|
||
</Container>
|
||
|
||
{/* Settings Dialog */}
|
||
<Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth>
|
||
<DialogTitle>Lobby Settings</DialogTitle>
|
||
<DialogContent>
|
||
<TextField
|
||
fullWidth
|
||
label="Lobby Name"
|
||
value={newGameName}
|
||
onChange={(e) => setNewGameName(e.target.value)}
|
||
margin="normal"
|
||
/>
|
||
<TextField
|
||
fullWidth
|
||
label="Description (Optional)"
|
||
value={newDescription}
|
||
onChange={(e) => setNewDescription(e.target.value)}
|
||
margin="normal"
|
||
multiline
|
||
rows={3}
|
||
placeholder="What's this game about?"
|
||
helperText="Add details about your game or rules"
|
||
/>
|
||
<TextField
|
||
fullWidth
|
||
label="New Password (optional)"
|
||
type="password"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
margin="normal"
|
||
helperText="Leave empty to remove password"
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setSettingsOpen(false)}>Cancel</Button>
|
||
<Button onClick={handleSetPassword} variant="outlined">
|
||
Update Password
|
||
</Button>
|
||
<Button onClick={handleSaveSettings} variant="contained">
|
||
Save Changes
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* Leave Confirmation Dialog */}
|
||
<Dialog open={leaveConfirmOpen} onClose={() => setLeaveConfirmOpen(false)} maxWidth="sm" fullWidth>
|
||
<DialogTitle sx={{ color: 'error.main', fontWeight: 'bold' }}>
|
||
⚠️ Are you SUPER SUPER SUPER sure?
|
||
</DialogTitle>
|
||
<DialogContent>
|
||
<Box sx={{ textAlign: 'center', py: 2 }}>
|
||
<Typography variant="h6" gutterBottom sx={{ color: 'error.main' }}>
|
||
You're about to leave "{lobby?.name}"
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||
This action cannot be undone! You will:
|
||
</Typography>
|
||
<Box sx={{ textAlign: 'left', mb: 2 }}>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• ❌ Be marked as "left" in the lobby
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 🚫 No longer participate in turns
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 📱 Stop receiving notifications
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 🔄 Need to be re-invited to rejoin
|
||
</Typography>
|
||
</Box>
|
||
<Typography variant="h6" sx={{ color: 'error.main', fontWeight: 'bold' }}>
|
||
Are you absolutely, positively, 100% certain?
|
||
</Typography>
|
||
</Box>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setLeaveConfirmOpen(false)} variant="contained" color="primary" size="large">
|
||
No, Keep Me In The Game!
|
||
</Button>
|
||
<Button onClick={confirmLeaveLobby} variant="outlined" color="error" size="large">
|
||
Yes, I'm Super Sure - Leave Forever
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* Invite Confirmation Dialog */}
|
||
<Dialog open={inviteConfirmOpen} onClose={handleDeclineInvite} maxWidth="sm" fullWidth>
|
||
<DialogTitle sx={{ color: 'primary.main', fontWeight: 'bold' }}>
|
||
🎮 Join Game Invitation
|
||
</DialogTitle>
|
||
<DialogContent>
|
||
<Box sx={{ textAlign: 'center', py: 2 }}>
|
||
<Typography variant="h6" gutterBottom>
|
||
You've been invited to join "{lobby?.name}"
|
||
</Typography>
|
||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||
Would you like to join this lobby and start playing?
|
||
</Typography>
|
||
<Box sx={{ textAlign: 'left', mb: 2 }}>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 🎯 Players: {lobby?.players.filter(p => !p.left).length}/10
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 👤 Owner: {lobby?.owner}
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 🔄 Current turn: {lobby?.players[lobby.turn]?.username || 'Loading...'}
|
||
</Typography>
|
||
{lobby?.password && (
|
||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||
• 🔒 Password protected
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
{lobby?.password && (
|
||
<TextField
|
||
fullWidth
|
||
label="Lobby Password"
|
||
type="password"
|
||
id="invite-password"
|
||
margin="normal"
|
||
placeholder="Enter lobby password"
|
||
/>
|
||
)}
|
||
</Box>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={handleDeclineInvite} variant="outlined" color="secondary" size="large">
|
||
No Thanks, Maybe Later
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
const passwordField = document.getElementById('invite-password') as HTMLInputElement
|
||
const password = passwordField?.value || undefined
|
||
handleJoinFromInvite(password)
|
||
}}
|
||
variant="contained"
|
||
color="primary"
|
||
size="large"
|
||
>
|
||
Yes, Join the Game!
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* Snackbar for notifications */}
|
||
<Snackbar
|
||
open={snackbar.open}
|
||
autoHideDuration={6000}
|
||
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||
message={snackbar.message}
|
||
/>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default LobbyPage
|
||
|