From 06e95fb4324d4dc90a93b923f94bdf93ba9948fe Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 11 Jan 2026 08:36:50 +0300 Subject: [PATCH] stiff --- admin/src/api/themes.ts | 6 +- admin/src/components/ThemeEditorDialog.tsx | 41 ++---- backend/prisma/seed.ts | 8 +- .../src/admin/themes/dto/create-theme.dto.ts | 7 +- backend/src/game/game.gateway.ts | 2 + backend/src/rooms/rooms.service.ts | 1 + src/components/Snowflakes.jsx | 31 ++-- src/context/ThemeContext.jsx | 74 ++++++++-- src/hooks/useRoom.js | 136 ++++++++++-------- src/pages/CreateRoom.jsx | 55 +------ src/pages/RoomPage.jsx | 51 +++---- 11 files changed, 186 insertions(+), 226 deletions(-) diff --git a/admin/src/api/themes.ts b/admin/src/api/themes.ts index 1a04837..a04cc21 100644 --- a/admin/src/api/themes.ts +++ b/admin/src/api/themes.ts @@ -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)', diff --git a/admin/src/components/ThemeEditorDialog.tsx b/admin/src/components/ThemeEditorDialog.tsx index 1f92965..cec9b6a 100644 --- a/admin/src/components/ThemeEditorDialog.tsx +++ b/admin/src/components/ThemeEditorDialog.tsx @@ -598,46 +598,25 @@ export function ThemeEditorDialog({

Animation Settings (Настройки анимации)

-
-
- - { - 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} /> - -