-
- Density (Плотность, частиц/Мп)
-
- {
- const value = parseInt(e.target.value, 10)
- if (!isNaN(value) && value > 0) {
- updateSetting('particleDensity', value)
- }
- }}
- />
-
- Плотность частиц (количество на 1Мп площади экрана). Учитывает размер экрана для одинаковой визуальной плотности на разных устройствах
+ Количество частиц на единицу ширины экрана. Обеспечивает одинаковую визуальную плотность на разных устройствах
diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts
index 8a67575..f4f96cc 100644
--- a/backend/prisma/seed.ts
+++ b/backend/prisma/seed.ts
@@ -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,
diff --git a/backend/src/admin/themes/dto/create-theme.dto.ts b/backend/src/admin/themes/dto/create-theme.dto.ts
index aede48a..7507c26 100644
--- a/backend/src/admin/themes/dto/create-theme.dto.ts
+++ b/backend/src/admin/themes/dto/create-theme.dto.ts
@@ -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()
diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts
index d92c68e..fac12a2 100644
--- a/backend/src/game/game.gateway.ts
+++ b/backend/src/game/game.gateway.ts
@@ -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);
}
diff --git a/backend/src/rooms/rooms.service.ts b/backend/src/rooms/rooms.service.ts
index 868b741..1910c1e 100644
--- a/backend/src/rooms/rooms.service.ts
+++ b/backend/src/rooms/rooms.service.ts
@@ -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);
diff --git a/src/components/Snowflakes.jsx b/src/components/Snowflakes.jsx
index 5e185ad..143b68a 100644
--- a/src/components/Snowflakes.jsx
+++ b/src/components/Snowflakes.jsx
@@ -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()
diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx
index 6e7e6dc..fdfcddb 100644
--- a/src/context/ThemeContext.jsx
+++ b/src/context/ThemeContext.jsx
@@ -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;
-
- const theme = themes.find((t) => t.id === currentTheme);
- if (!theme) 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 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);
}
};
diff --git a/src/hooks/useRoom.js b/src/hooks/useRoom.js
index afcf926..93884e7 100644
--- a/src/hooks/useRoom.js
+++ b/src/hooks/useRoom.js
@@ -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();
diff --git a/src/pages/CreateRoom.jsx b/src/pages/CreateRoom.jsx
index 30765c9..87e2e56 100644
--- a/src/pages/CreateRoom.jsx
+++ b/src/pages/CreateRoom.jsx
@@ -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 = () => {
-
-
-
- setIsHostNameModalOpen(false)}
- title="Введите ваше имя как ведущего"
- description="Чтобы создать комнату, введите ваше имя как ведущего"
- />
);
};
diff --git a/src/pages/RoomPage.jsx b/src/pages/RoomPage.jsx
index 60bb876..823cd86 100644
--- a/src/pages/RoomPage.jsx
+++ b/src/pages/RoomPage.jsx
@@ -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}
/>
-
-