From 625c5837ee767c64fd584d6835db4ea42df88bf2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 11 Jan 2026 00:42:10 +0300 Subject: [PATCH] stuff --- backend/src/game/game.gateway.ts | 91 +++++++++++++++----------- src/components/GameManagementModal.jsx | 72 ++++++++++++++------ src/pages/GamePage.jsx | 22 ++++--- src/pages/RoomPage.jsx | 86 ++++++++++++------------ 4 files changed, 161 insertions(+), 110 deletions(-) diff --git a/backend/src/game/game.gateway.ts b/backend/src/game/game.gateway.ts index 66e8281..5c8b820 100644 --- a/backend/src/game/game.gateway.ts +++ b/backend/src/game/game.gateway.ts @@ -739,50 +739,58 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On return; } - // Обновляем вопросы через service (который добавит UUID если нужно) - await this.roomsService.updateRoomPack(payload.roomId, payload.questions); - - // После обновления вопросов проверяем и обновляем currentQuestionId - const room = (await this.prisma.room.findUnique({ - where: { id: payload.roomId }, - include: { roomPack: true } as unknown as Prisma.RoomInclude, - })) as unknown as RoomWithPack | null; - - if (room) { - const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[]; - const currentQuestionId = (room.currentQuestionId as string | null) || null; + try { + // Убеждаемся, что questions - это массив + const questionsArray = Array.isArray(payload.questions) ? payload.questions : []; - // Проверяем, что currentQuestionId все еще валиден - let validQuestionId = currentQuestionId; - if (currentQuestionId) { - const questionExists = questions.some((q: any) => q.id === currentQuestionId); - if (!questionExists) { - // Текущий вопрос был удален, устанавливаем первый - validQuestionId = questions[0]?.id && typeof questions[0].id === 'string' + // Обновляем вопросы через service (который добавит UUID если нужно) + await this.roomsService.updateRoomPack(payload.roomId, questionsArray); + + // После обновления вопросов проверяем и обновляем currentQuestionId + const room = (await this.prisma.room.findUnique({ + where: { id: payload.roomId }, + include: { roomPack: true } as unknown as Prisma.RoomInclude, + })) as unknown as RoomWithPack | null; + + if (room) { + const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[]; + const currentQuestionId = (room.currentQuestionId as string | null) || null; + + // Проверяем, что currentQuestionId все еще валиден + let validQuestionId = currentQuestionId; + if (currentQuestionId) { + const questionExists = questions.some((q: any) => q.id === currentQuestionId); + if (!questionExists) { + // Текущий вопрос был удален, устанавливаем первый + validQuestionId = questions[0]?.id && typeof questions[0].id === 'string' + ? questions[0].id + : null; + } + } else if (questions.length > 0) { + // Если нет currentQuestionId, устанавливаем первый + validQuestionId = questions[0].id && typeof questions[0].id === 'string' ? questions[0].id : null; } - } else if (questions.length > 0) { - // Если нет currentQuestionId, устанавливаем первый - validQuestionId = questions[0].id && typeof questions[0].id === 'string' - ? questions[0].id - : null; + + // Обновляем currentQuestionId если изменился + if (validQuestionId !== currentQuestionId) { + const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId); + await this.prisma.room.update({ + where: { id: payload.roomId }, + data: { + currentQuestionId: validQuestionId, + currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0 + } + }); + } } - // Обновляем currentQuestionId если изменился - if (validQuestionId !== currentQuestionId) { - const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId); - await this.prisma.room.update({ - where: { id: payload.roomId }, - data: { - currentQuestionId: validQuestionId, - currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0 - } - }); - } + await this.broadcastFullState(payload.roomCode); + } catch (error: any) { + console.error('Error updating room pack:', error); + client.emit('error', { message: error.message || 'Failed to update questions' }); } - - await this.broadcastFullState(payload.roomCode); } @SubscribeMessage('importQuestions') @@ -810,8 +818,13 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On return; } - await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices); - await this.broadcastFullState(payload.roomCode); + try { + await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices); + await this.broadcastFullState(payload.roomCode); + } catch (error: any) { + console.error('Error importing questions:', error); + client.emit('error', { message: error.message || 'Failed to import questions' }); + } } @SubscribeMessage('kickPlayer') diff --git a/src/components/GameManagementModal.jsx b/src/components/GameManagementModal.jsx index 1aca3e3..4e8ad9b 100644 --- a/src/components/GameManagementModal.jsx +++ b/src/components/GameManagementModal.jsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react' import { questionsApi } from '../services/api' import { useTheme } from '../context/ThemeContext' +import { useAuth } from '../context/AuthContext' +import socketService from '../services/socket' import './GameManagementModal.css' import './QuestionsModal.css' @@ -37,6 +39,7 @@ const GameManagementModal = ({ onAddPlayer, }) => { const { currentThemeData } = useTheme() + const { user } = useAuth() const [activeTab, setActiveTab] = useState(initialTab) // players | game | scoring | questions const [selectedPlayer, setSelectedPlayer] = useState(null) const [customPoints, setCustomPoints] = useState(10) @@ -96,6 +99,24 @@ const GameManagementModal = ({ } }, [isOpen, initialTab]) + // Обработка WebSocket ошибок для операций с вопросами + useEffect(() => { + if (!isOpen) return + + const handleError = (error) => { + console.error('WebSocket error in GameManagementModal:', error) + if (error.message) { + setJsonError(error.message) + } + } + + socketService.on('error', handleError) + + return () => { + socketService.off('error', handleError) + } + }, [isOpen]) + if (!isOpen) return null const gameStatus = room?.status || 'WAITING' @@ -256,13 +277,16 @@ const GameManagementModal = ({ })), } + // Убеждаемся, что questions - это массив + const currentQuestions = Array.isArray(questions) ? questions : [] + let updatedQuestions if (editingQuestion) { - updatedQuestions = questions.map(q => + updatedQuestions = currentQuestions.map(q => q.id === editingQuestion.id ? questionData : q ) } else { - updatedQuestions = [...questions, questionData] + updatedQuestions = [...currentQuestions, questionData] } onUpdateQuestions(updatedQuestions) @@ -577,27 +601,35 @@ const GameManagementModal = ({ } const handleImportSelected = () => { + if (!room?.id || !room?.code || !user?.id || !selectedPack) { + setJsonError('Ошибка: недостаточно данных для импорта') + return + } + const indices = Array.from(selectedQuestionIndices) - const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean) + if (indices.length === 0) { + setJsonError('Выберите хотя бы один вопрос для импорта') + return + } - const copiedQuestions = questionsToImport.map((q) => ({ - id: crypto.randomUUID(), - text: q.text || '', - answers: (q.answers || []).map(a => ({ - id: a.id || crypto.randomUUID(), - text: a.text, - points: a.points - })), - })) + try { + socketService.importQuestions( + room.id, + room.code, + user.id, + selectedPack, + indices + ) - const updatedQuestions = [...questions, ...copiedQuestions] - onUpdateQuestions(updatedQuestions) - - setSelectedQuestionIndices(new Set()) - setSearchQuery('') - setShowPackImportModal(false) - setJsonError('') - alert(`Импортировано ${copiedQuestions.length} вопросов`) + setSelectedQuestionIndices(new Set()) + setSearchQuery('') + setShowPackImportModal(false) + setJsonError('') + // Уведомление будет показано через WebSocket событие gameStateUpdated + } catch (error) { + console.error('Error importing questions:', error) + setJsonError('Ошибка при импорте вопросов: ' + (error.message || 'Неизвестная ошибка')) + } } return ( diff --git a/src/pages/GamePage.jsx b/src/pages/GamePage.jsx index 3ceddc3..ec04d5c 100644 --- a/src/pages/GamePage.jsx +++ b/src/pages/GamePage.jsx @@ -46,7 +46,9 @@ const GamePage = () => { // Храним participantId текущего пользователя для проверки удаления const currentUserParticipantIdRef = useRef(null); // Храним предыдущий themeId комнаты для отслеживания изменений - const previousThemeIdRef = useRef(null); + // Используем undefined для обозначения первой загрузки (не установлено), + // null означает "тема не установлена в комнате" + const previousThemeIdRef = useRef(undefined); // ЕДИНСТВЕННЫЙ обработчик состояния игры useEffect(() => { @@ -65,13 +67,14 @@ const GamePage = () => { currentUserParticipantIdRef.current = currentUserParticipant?.id || null; } - // Применяем тему комнаты, если она изменилась + // Применяем тему комнаты при первой загрузке или если она изменилась const currentThemeId = state.themeId || null; - if (currentThemeId !== previousThemeIdRef.current) { + const isFirstLoad = previousThemeIdRef.current === undefined; + + if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) { previousThemeIdRef.current = currentThemeId; - if (currentThemeId) { - changeTheme(currentThemeId); - } + // Применяем тему (даже если null - вернемся к дефолтной теме) + changeTheme(currentThemeId); } }; @@ -268,14 +271,17 @@ const GamePage = () => { }; const handleUpdateRoomQuestions = async (newQuestions) => { - if (!gameState.roomId) return; + if (!gameState.roomId || !user?.id) return; try { + // Убеждаемся, что newQuestions - это массив + const questionsArray = Array.isArray(newQuestions) ? newQuestions : []; + socketService.emit('updateRoomPack', { roomId: gameState.roomId, roomCode: gameState.roomCode, userId: user.id, - questions: newQuestions + questions: questionsArray }); } catch (error) { console.error('Error updating room pack:', error); diff --git a/src/pages/RoomPage.jsx b/src/pages/RoomPage.jsx index 13249ae..7cf06a5 100644 --- a/src/pages/RoomPage.jsx +++ b/src/pages/RoomPage.jsx @@ -19,7 +19,8 @@ const RoomPage = () => { const { changeTheme } = useTheme(); // Храним предыдущий themeId комнаты для отслеживания изменений - const previousThemeIdRef = useRef(null); + // Используем undefined для обозначения первой загрузки (не установлено) + const previousThemeIdRef = useRef(undefined); // Callback для автоматической навигации при старте игры const handleGameStartedEvent = useCallback(() => { @@ -120,44 +121,39 @@ const RoomPage = () => { } }; - // Показываем модальное окно выбора роли, если allowSpectators === true и пользователь авторизован - useEffect(() => { - if ( - room && - user && - !joined && - !isRoleSelectionModalOpen && - room.allowSpectators && - !participants.some((p) => p.userId === user.id) - ) { - setIsRoleSelectionModalOpen(true); - } - }, [room, user, joined, participants, isRoleSelectionModalOpen]); - - // Автоматическое присоединение как PLAYER, если зрители не разрешены + // Единая логика присоединения к комнате useEffect(() => { const handleJoin = async () => { - if (room && user && !joined && !isRoleSelectionModalOpen) { - const isParticipant = participants.some((p) => p.userId === user.id); - if (!isParticipant) { - // Если зрители не разрешены, присоединяемся как PLAYER автоматически - // Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED) - if (!room.allowSpectators) { - try { - setJoinError(null); - await joinRoom(room.id, user.id, user.name || 'Гость', 'PLAYER'); - setJoined(true); - // Если игра уже началась, перенаправление произойдет после обновления participants - } catch (error) { - console.error('Join error:', error); - const errorMessage = error.response?.data?.message || error.message || 'Ошибка при присоединении к комнате'; - setJoinError(errorMessage); - alert(errorMessage); - } - } - } else { - setJoined(true); + // Пропускаем если нет комнаты, пользователя или уже присоединились + if (!room || !user || joined) return; + + // Проверяем, не является ли пользователь уже участником + const isParticipant = participants.some((p) => p.userId === user.id); + if (isParticipant) { + setJoined(true); + return; + } + + // Если зрители разрешены, показываем модальное окно выбора роли + if (room.allowSpectators) { + if (!isRoleSelectionModalOpen) { + setIsRoleSelectionModalOpen(true); } + return; + } + + // Если зрители не разрешены, автоматически присоединяемся как PLAYER + // Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED) + try { + setJoinError(null); + await joinRoom(room.id, user.id, user.name || 'Гость', 'PLAYER'); + setJoined(true); + // Если игра уже началась, перенаправление произойдет после обновления participants + } catch (error) { + console.error('Join error:', error); + const errorMessage = error.response?.data?.message || error.message || 'Ошибка при присоединении к комнате'; + setJoinError(errorMessage); + alert(errorMessage); } }; @@ -228,12 +224,13 @@ const RoomPage = () => { const handleGameStateUpdated = (state) => { const currentThemeId = state.themeId || null; - // Применяем тему если она изменилась или если это первое присоединение (previousThemeIdRef.current === null) - if (currentThemeId !== previousThemeIdRef.current) { + const isFirstLoad = previousThemeIdRef.current === undefined; + + // Применяем тему при первой загрузке или если она изменилась + if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) { previousThemeIdRef.current = currentThemeId; - if (currentThemeId) { - changeTheme(currentThemeId); - } + // Применяем тему (даже если null - вернемся к дефолтной теме) + changeTheme(currentThemeId); } }; @@ -248,9 +245,12 @@ const RoomPage = () => { if (!roomCode || !room) return; const currentThemeId = room.themeId || null; - // Применяем тему если она существует и еще не применена (первое присоединение) или изменилась - if (currentThemeId && currentThemeId !== previousThemeIdRef.current) { + const isFirstLoad = previousThemeIdRef.current === undefined; + + // Применяем тему при первой загрузке или если она изменилась + if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) { previousThemeIdRef.current = currentThemeId; + // Применяем тему (даже если null - вернемся к дефолтной теме) changeTheme(currentThemeId); } }, [roomCode, room?.themeId, changeTheme]);