This commit is contained in:
Dmitry 2026-01-11 08:36:50 +03:00
parent 403ea8ac24
commit 06e95fb432
11 changed files with 186 additions and 226 deletions

View file

@ -29,12 +29,11 @@ export interface ThemeSettings {
particleSymbol?: string
particleColor?: string
particleGlow?: string
particleTargetCount?: number
particleSpawnCount?: number
particleUpdateInterval?: number
particleDurationMin?: number
particleDurationMax?: number
particleInitialDelayMax?: number
particleDensity?: number
// Finish Screen Settings
finishScreenTitle?: string
finishScreenSubtitle?: string
@ -313,12 +312,11 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
particleSymbol: '❄',
particleColor: '#ffffff',
particleGlow: 'rgba(255, 255, 255, 0.8)',
particleTargetCount: 200,
particleSpawnCount: 0.1,
particleUpdateInterval: 1000,
particleDurationMin: 7,
particleDurationMax: 10,
particleInitialDelayMax: 10,
particleDensity: 100,
finishScreenTitle: 'Игра завершена!',
finishScreenSubtitle: '',
finishScreenBgColor: 'rgba(0, 0, 0, 0.5)',

View file

@ -598,46 +598,25 @@ export function ThemeEditorDialog({
<h4 className="text-md font-medium">Animation Settings (Настройки анимации)</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="particleTargetCount">
Target Count (Целевое количество)
<Label htmlFor="particleSpawnCount">
Spawn Count (частиц/пиксель ширины)
</Label>
<Input
id="particleTargetCount"
id="particleSpawnCount"
type="number"
min="1"
max="1000"
value={settings.particleTargetCount ?? DEFAULT_THEME_SETTINGS.particleTargetCount ?? 200}
min="0.01"
max="10"
step="0.01"
value={settings.particleSpawnCount ?? DEFAULT_THEME_SETTINGS.particleSpawnCount ?? 0.1}
onChange={(e) => {
const value = parseInt(e.target.value, 10)
const value = parseFloat(e.target.value)
if (!isNaN(value) && value > 0) {
updateSetting('particleTargetCount', value)
updateSetting('particleSpawnCount', value)
}
}}
/>
<p className="text-xs text-muted-foreground">
Целевое количество снежинок на экране (используется, если плотность не задана)
</p>
</div>
<div className="space-y-2">
<Label htmlFor="particleDensity">
Density (Плотность, частиц/Мп)
</Label>
<Input
id="particleDensity"
type="number"
min="1"
max="500"
step="1"
value={settings.particleDensity ?? DEFAULT_THEME_SETTINGS.particleDensity ?? 100}
onChange={(e) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value > 0) {
updateSetting('particleDensity', value)
}
}}
/>
<p className="text-xs text-muted-foreground">
Плотность частиц (количество на 1Мп площади экрана). Учитывает размер экрана для одинаковой визуальной плотности на разных устройствах
Количество частиц на единицу ширины экрана. Обеспечивает одинаковую визуальную плотность на разных устройствах
</p>
</div>
<div className="space-y-2">

View file

@ -287,7 +287,7 @@ async function main() {
particleSymbol: '❄',
particleColor: '#ffffff',
particleGlow: 'rgba(255, 215, 0, 0.8)',
particleTargetCount: 200,
particleSpawnCount: 0.1,
particleUpdateInterval: 1000,
particleDurationMin: 7,
particleDurationMax: 10,
@ -327,7 +327,7 @@ async function main() {
particleSymbol: '🌸',
particleColor: '#2d3748',
particleGlow: 'rgba(47, 128, 237, 0.6)',
particleTargetCount: 200,
particleSpawnCount: 0.1,
particleUpdateInterval: 1000,
particleDurationMin: 7,
particleDurationMax: 10,
@ -367,7 +367,7 @@ async function main() {
particleSymbol: '🎉',
particleColor: '#ffffff',
particleGlow: 'rgba(255, 87, 108, 0.8)',
particleTargetCount: 200,
particleSpawnCount: 0.1,
particleUpdateInterval: 1000,
particleDurationMin: 7,
particleDurationMax: 10,
@ -407,7 +407,7 @@ async function main() {
particleSymbol: '✨',
particleColor: '#e0e0e0',
particleGlow: 'rgba(100, 255, 218, 0.6)',
particleTargetCount: 200,
particleSpawnCount: 0.1,
particleUpdateInterval: 1000,
particleDurationMin: 7,
particleDurationMax: 10,

View file

@ -90,7 +90,7 @@ export class ThemeSettingsDto {
@IsNumber()
@IsOptional()
@Type(() => Number)
particleTargetCount?: number;
particleSpawnCount?: number;
@IsNumber()
@IsOptional()
@ -112,11 +112,6 @@ export class ThemeSettingsDto {
@Type(() => Number)
particleInitialDelayMax?: number;
@IsNumber()
@IsOptional()
@Type(() => Number)
particleDensity?: number;
// Finish Screen Settings
@IsString()
@IsOptional()

View file

@ -102,6 +102,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
@SubscribeMessage('joinRoom')
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
console.log(`🔌 Client ${client.id} joining WebSocket room ${payload.roomCode}, userId: ${payload.userId}`);
client.join(payload.roomCode);
// Получаем полное состояние для отправки присоединившемуся клиенту
@ -573,6 +574,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
})
};
console.log(`📡 Broadcasting gameStateUpdated to room ${roomCode} with ${room.participants.length} participants`);
this.server.to(roomCode).emit('gameStateUpdated', fullState);
}

View file

@ -157,6 +157,7 @@ export class RoomsService {
// WebSocket joinRoom может выполняться параллельно с REST API joinRoom
await new Promise(resolve => setTimeout(resolve, 50));
console.log(`📤 Broadcasting room update for ${updatedRoom.code} with ${updatedRoom.participants.length} participants`);
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
// Также отправляем gameStateUpdated через broadcastFullState
await this.gameGateway.broadcastFullState(updatedRoom.code);

View file

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useTheme } from '../context/ThemeContext'
// Default values for particle animation settings
const DEFAULT_TARGET_COUNT = 200
const DEFAULT_SPAWN_COUNT = 0.1
const DEFAULT_UPDATE_INTERVAL = 1000
const DEFAULT_DURATION_MIN = 7
const DEFAULT_DURATION_MAX = 10
@ -53,29 +53,16 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
return currentThemeData?.settings?.particleSymbol || '❄'
}
// Get particle density from theme settings
const getParticleDensity = () => {
return currentThemeData?.settings?.particleDensity
// Get particle spawn count from theme settings (particles per pixel width)
const getParticleSpawnCount = () => {
return currentThemeData?.settings?.particleSpawnCount ?? DEFAULT_SPAWN_COUNT
}
// Calculate target count based on density and screen size, or use fixed target count
// Calculate target count based on spawn count and screen width
const calculateTargetCount = () => {
const density = getParticleDensity()
// If density is set, calculate based on screen area (particles per 1Mp)
if (density !== undefined && density > 0) {
const area = windowSize.width * windowSize.height
const count = Math.round((density * area) / 1000000)
return Math.max(1, count) // Ensure at least 1 particle
}
// Fallback to fixed target count
return currentThemeData?.settings?.particleTargetCount ?? DEFAULT_TARGET_COUNT
}
// Get particle animation settings from theme with defaults
const getParticleTargetCount = () => {
return calculateTargetCount()
const spawnCount = getParticleSpawnCount()
const count = Math.round(spawnCount * windowSize.width)
return Math.max(1, count) // Ensure at least 1 particle
}
const getParticleUpdateInterval = () => {
@ -104,7 +91,7 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
const particlesEnabled = getParticlesEnabled()
const particleSymbol = getParticleSymbol()
const targetCount = getParticleTargetCount()
const targetCount = calculateTargetCount()
const updateInterval = getParticleUpdateInterval()
const durationRange = getParticleDurationRange()

View file

@ -18,8 +18,23 @@ export const ThemeProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
const [pendingThemeId, setPendingThemeId] = useState(null);
const [currentTheme, setCurrentTheme] = useState(() => {
// Загружаем полный объект темы из localStorage
const saved = localStorage.getItem('app-theme');
return saved || null;
if (saved) {
try {
const themeData = JSON.parse(saved);
// Если это объект темы, возвращаем его ID
if (themeData && typeof themeData === 'object' && themeData.id) {
return themeData.id;
}
// Если это просто строка ID (старый формат), возвращаем её
return themeData;
} catch (e) {
console.error('Failed to parse theme from localStorage:', e);
return null;
}
}
return null;
});
// Load themes from API
@ -38,9 +53,16 @@ export const ThemeProvider = ({ children }) => {
// Find theme marked as default, or use first theme as fallback
const defaultTheme = data.find((t) => t.isDefault === true) || data[0];
if (defaultTheme) {
localStorage.setItem('app-theme', defaultTheme.id);
// Сохраняем полный объект темы
localStorage.setItem('app-theme', JSON.stringify(defaultTheme));
return defaultTheme.id;
}
} else {
// Обновляем сохраненную тему актуальными данными с сервера
const currentThemeData = data.find((t) => t.id === prevTheme);
if (currentThemeData) {
localStorage.setItem('app-theme', JSON.stringify(currentThemeData));
}
}
return prevTheme;
});
@ -69,11 +91,38 @@ export const ThemeProvider = ({ children }) => {
// Apply theme when currentTheme or themes change
useEffect(() => {
if (!currentTheme || themes.length === 0) return;
// Сначала пытаемся применить тему из localStorage для мгновенного отображения
const applyThemeFromCache = () => {
const savedThemeStr = localStorage.getItem('app-theme');
if (savedThemeStr) {
try {
const savedTheme = JSON.parse(savedThemeStr);
if (savedTheme && typeof savedTheme === 'object' && savedTheme.colors && savedTheme.settings) {
applyThemeStyles(savedTheme);
return true;
}
} catch (e) {
console.error('Failed to apply cached theme:', e);
}
}
return false;
};
const theme = themes.find((t) => t.id === currentTheme);
if (!theme) return;
// Применяем кэшированную тему сразу
const cacheApplied = applyThemeFromCache();
// Затем применяем актуальную тему из загруженного списка (если она отличается)
if (currentTheme && themes.length > 0) {
const theme = themes.find((t) => t.id === currentTheme);
if (theme) {
applyThemeStyles(theme);
// Сохраняем полный объект темы
localStorage.setItem('app-theme', JSON.stringify(theme));
}
}
}, [currentTheme, themes]);
const applyThemeStyles = (theme) => {
const root = document.documentElement;
// Remove data-theme attribute (for built-in CSS themes)
@ -92,7 +141,7 @@ export const ThemeProvider = ({ children }) => {
// Skip boolean and number values - they are handled separately (numbers for JS, booleans for logic)
// Only string values are used as CSS variables (except particle animation numbers)
const isParticleNumber = [
'particleTargetCount',
'particleSpawnCount',
'particleUpdateInterval',
'particleDurationMin',
'particleDurationMax',
@ -117,9 +166,7 @@ export const ThemeProvider = ({ children }) => {
root.style.setProperty('--particle-color', particleColor);
root.style.setProperty('--particle-glow', particleGlow);
}
localStorage.setItem('app-theme', currentTheme);
}, [currentTheme, themes]);
};
const changeTheme = (themeId) => {
// Если темы еще не загружены, сохраняем themeId для применения после загрузки
@ -129,8 +176,11 @@ export const ThemeProvider = ({ children }) => {
}
// Если тема существует в списке, применяем её
if (themes.find((t) => t.id === themeId)) {
const theme = themes.find((t) => t.id === themeId);
if (theme) {
setCurrentTheme(themeId);
// Сохраняем полный объект темы
localStorage.setItem('app-theme', JSON.stringify(theme));
setPendingThemeId(null);
}
};

View file

@ -4,19 +4,83 @@ import socketService from '../services/socket';
import { useAuth } from '../context/AuthContext';
export const useRoom = (roomCode, onGameStarted = null, password = null) => {
const { user } = useAuth();
const { user, loading: authLoading } = useAuth();
const [room, setRoom] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [participants, setParticipants] = useState([]);
const [requiresPassword, setRequiresPassword] = useState(false);
// Обработчики событий вынесены наружу useEffect, чтобы они регистрировались только один раз
// и не зависели от изменений зависимостей
const handleRoomUpdate = useCallback((updatedRoom) => {
console.log('📨 roomUpdate received:', updatedRoom.participants?.length, 'participants');
setRoom(updatedRoom);
setParticipants(updatedRoom.participants || []);
}, []);
const handleGameStarted = useCallback((updatedRoom) => {
console.log('🎮 gameStarted received');
setRoom(updatedRoom);
// Вызываем callback для навигации на страницу игры
if (onGameStarted) {
onGameStarted(updatedRoom);
}
}, [onGameStarted]);
const handleGameStateUpdated = useCallback((state) => {
console.log('🔄 gameStateUpdated received:', state.participants?.length, 'participants');
// Обновляем только базовую информацию о комнате
// Полное состояние игры управляется в GamePage
if (state.participants) {
setParticipants(state.participants);
}
// Обновляем базовую информацию о комнате из состояния
setRoom(prevRoom => {
if (!prevRoom) return prevRoom;
const updatedRoom = { ...prevRoom };
if (state.status) {
updatedRoom.status = state.status;
}
if (state.themeId !== undefined) {
updatedRoom.themeId = state.themeId;
}
if (state.voiceMode !== undefined) {
updatedRoom.voiceMode = state.voiceMode;
}
return updatedRoom;
});
// Если игра началась, вызываем callback
if (state.status === 'PLAYING' && onGameStarted) {
onGameStarted(state);
}
}, [onGameStarted]);
const handleRoomPackUpdated = useCallback((updatedRoom) => {
setRoom(updatedRoom);
if (updatedRoom.participants) {
setParticipants(updatedRoom.participants);
}
}, []);
useEffect(() => {
if (!roomCode) {
setLoading(false);
return;
}
// Ждем загрузки пользователя из куки перед запросом комнаты
if (authLoading) {
return;
}
const fetchRoom = async () => {
try {
setLoading(true);
@ -36,8 +100,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
setError(null);
setRequiresPassword(false);
// Сохраняем пароль, если вход успешен
if (roomPassword) {
// Сохраняем пароль, если вход успешен И пользователь не является хостом
if (roomPassword && response.data.hostId !== user?.id) {
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
}
@ -64,63 +128,7 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
fetchRoom();
// Listen for room updates (регистрируются всегда, но не подключаются если requiresPassword)
const handleRoomUpdate = (updatedRoom) => {
setRoom(updatedRoom);
setParticipants(updatedRoom.participants || []);
};
const handleGameStarted = (updatedRoom) => {
setRoom(updatedRoom);
// Вызываем callback для навигации на страницу игры
if (onGameStarted) {
onGameStarted(updatedRoom);
}
};
// Используем новое событие gameStateUpdated если нужно обновлять состояние
const handleGameStateUpdated = (state) => {
// Обновляем только базовую информацию о комнате
// Полное состояние игры управляется в GamePage
if (state.participants) {
setParticipants(state.participants);
}
// Обновляем базовую информацию о комнате из состояния
setRoom(prevRoom => {
if (!prevRoom) return prevRoom;
const updatedRoom = { ...prevRoom };
if (state.status) {
updatedRoom.status = state.status;
}
if (state.themeId !== undefined) {
updatedRoom.themeId = state.themeId;
}
if (state.voiceMode !== undefined) {
updatedRoom.voiceMode = state.voiceMode;
}
return updatedRoom;
});
// Если игра началась, вызываем callback
if (state.status === 'PLAYING' && onGameStarted) {
onGameStarted(state);
}
};
// Обработчик обновления вопросов комнаты
const handleRoomPackUpdated = (updatedRoom) => {
setRoom(updatedRoom);
if (updatedRoom.participants) {
setParticipants(updatedRoom.participants);
}
};
// Регистрируем обработчики событий
socketService.on('roomUpdate', handleRoomUpdate);
socketService.on('gameStarted', handleGameStarted);
socketService.on('gameStateUpdated', handleGameStateUpdated);
@ -132,7 +140,7 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
socketService.off('gameStateUpdated', handleGameStateUpdated);
socketService.off('roomPackUpdated', handleRoomPackUpdated);
};
}, [roomCode, password, onGameStarted, user?.id]);
}, [roomCode, password, user?.id, authLoading, handleRoomUpdate, handleGameStarted, handleGameStateUpdated, handleRoomPackUpdated]);
const createRoom = useCallback(async (hostId, questionPackId, settings = {}, hostName) => {
try {
@ -203,8 +211,10 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
setError(null);
setRequiresPassword(false);
// Сохраняем пароль в localStorage при успешном входе
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
// Сохраняем пароль в localStorage при успешном входе, НО НЕ для хоста
if (response.data.hostId !== user?.id) {
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
}
// ✅ После успешной загрузки комнаты с паролем подключаемся к WebSocket
socketService.connect();

View file

@ -1,12 +1,11 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useRoom } from '../hooks/useRoom';
import NameInputModal from '../components/NameInputModal';
const CreateRoom = () => {
const navigate = useNavigate();
const { user, loginAnonymous, loading: authLoading } = useAuth();
const { user } = useAuth();
const { createRoom, loading: roomLoading } = useRoom();
const [settings, setSettings] = useState({
@ -14,42 +13,14 @@ const CreateRoom = () => {
allowSpectators: true,
password: '',
});
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
const [isHostNameModalOpen, setIsHostNameModalOpen] = useState(false);
// Проверка авторизации и показ модального окна для ввода имени
useEffect(() => {
if (!authLoading && !user) {
setIsNameModalOpen(true);
} else if (user) {
setIsNameModalOpen(false);
}
}, [authLoading, user]);
// Обработка ввода имени и авторизация
const handleNameSubmit = async (name) => {
try {
await loginAnonymous(name);
setIsNameModalOpen(false);
} catch (error) {
console.error('Login error:', error);
alert('Ошибка при авторизации. Попробуйте еще раз.');
}
};
const handleCreateRoom = async () => {
if (!user) {
setIsNameModalOpen(true);
alert('Пожалуйста, задайте имя на главном экране');
navigate('/');
return;
}
// Всегда спрашиваем имя хоста перед созданием комнаты
setIsHostNameModalOpen(true);
};
const handleHostNameSubmit = async (name) => {
setIsHostNameModalOpen(false);
try {
// Очищаем пустой пароль перед отправкой
const cleanSettings = { ...settings };
@ -63,7 +34,7 @@ const CreateRoom = () => {
user.id,
undefined,
cleanSettings,
name.trim(),
user.name,
);
navigate(`/room/${room.code}`);
} catch (error) {
@ -121,7 +92,7 @@ const CreateRoom = () => {
<div className="button-group">
<button
onClick={handleCreateRoom}
disabled={roomLoading}
disabled={roomLoading || !user}
className="primary"
>
{roomLoading ? 'Создание...' : 'Создать комнату'}
@ -129,20 +100,6 @@ const CreateRoom = () => {
<button onClick={() => navigate('/')}>Назад</button>
</div>
</div>
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={null}
/>
<NameInputModal
isOpen={isHostNameModalOpen}
onSubmit={handleHostNameSubmit}
onCancel={() => setIsHostNameModalOpen(false)}
title="Введите ваше имя как ведущего"
description="Чтобы создать комнату, введите ваше имя как ведущего"
/>
</div>
);
};

View file

@ -7,7 +7,6 @@ import { questionsApi } from '../services/api';
import QRCode from 'qrcode';
import socketService from '../services/socket';
import QRModal from '../components/QRModal';
import NameInputModal from '../components/NameInputModal';
import PasswordModal from '../components/PasswordModal';
import RoleSelectionModal from '../components/RoleSelectionModal';
import GameManagementModal from '../components/GameManagementModal';
@ -15,7 +14,7 @@ import GameManagementModal from '../components/GameManagementModal';
const RoomPage = () => {
const { roomCode } = useParams();
const navigate = useNavigate();
const { user, loginAnonymous, loading: authLoading } = useAuth();
const { user } = useAuth();
const { changeTheme } = useTheme();
// Храним предыдущий themeId комнаты для отслеживания изменений
@ -41,7 +40,6 @@ const RoomPage = () => {
const [qrCode, setQrCode] = useState('');
const [joined, setJoined] = useState(false);
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false);
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
@ -53,6 +51,11 @@ const RoomPage = () => {
// Ref для отслеживания попытки присоединения (защита от двойного запроса)
const joinAttemptedRef = useRef(false);
// Логирование изменений participants для отладки
useEffect(() => {
console.log('👥 RoomPage participants updated:', participants.length, participants.map(p => p.name));
}, [participants]);
useEffect(() => {
const generateQR = async () => {
try {
@ -78,24 +81,12 @@ const RoomPage = () => {
}, [roomCode]);
// Проверка пароля: показываем модальное окно, если требуется пароль
// Показываем независимо от авторизации - пароль проверяется первым
useEffect(() => {
if (requiresPassword && !isPasswordModalOpen && !loading) {
// Показывать модальное окно пароля независимо от авторизации
setIsPasswordModalOpen(true);
}
}, [requiresPassword, isPasswordModalOpen, loading]);
// Проверка авторизации и показ модального окна для ввода имени
// Показывать только если НЕТ пароля - пароль приоритетнее
useEffect(() => {
if (!authLoading && !user && room && !loading && !requiresPassword) {
setIsNameModalOpen(true);
} else if (user) {
setIsNameModalOpen(false);
}
}, [authLoading, user, room, loading, requiresPassword]);
// Обработка ввода пароля
const handlePasswordSubmit = async (enteredPassword) => {
try {
@ -113,22 +104,18 @@ const RoomPage = () => {
}
};
// Обработка ввода имени и авторизация
const handleNameSubmit = async (name) => {
try {
await loginAnonymous(name);
setIsNameModalOpen(false);
} catch (error) {
console.error('Login error:', error);
alert('Ошибка при авторизации. Попробуйте еще раз.');
}
};
// Единая логика присоединения к комнате
useEffect(() => {
const handleJoin = async () => {
// Пропускаем если нет комнаты, пользователя или уже присоединились
if (!room || !user || joined) return;
// Перенаправляем на главный экран, если нет пользователя
if (!user) {
alert('Пожалуйста, задайте имя на главном экране');
navigate('/');
return;
}
// Пропускаем если нет комнаты или уже присоединились
if (!room || joined) return;
// Проверяем, не является ли пользователь уже участником
const isParticipant = participants.some((p) => p.userId === user.id);
@ -169,7 +156,7 @@ const RoomPage = () => {
};
handleJoin();
}, [room, user, participants, joined, joinRoom, isRoleSelectionModalOpen]);
}, [room, user, joined, participants, joinRoom, navigate, isRoleSelectionModalOpen]);
// Обработка выбора роли
// Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED)
@ -437,12 +424,6 @@ const RoomPage = () => {
roomCode={roomCode}
/>
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={null}
/>
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}