This commit is contained in:
Dmitry 2026-01-11 00:42:10 +03:00
parent 13ecd4ff57
commit 625c5837ee
4 changed files with 161 additions and 110 deletions

View file

@ -739,50 +739,58 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
return; return;
} }
// Обновляем вопросы через service (который добавит UUID если нужно) try {
await this.roomsService.updateRoomPack(payload.roomId, payload.questions); // Убеждаемся, что questions - это массив
const questionsArray = Array.isArray(payload.questions) ? payload.questions : [];
// После обновления вопросов проверяем и обновляем currentQuestionId // Обновляем вопросы через service (который добавит UUID если нужно)
const room = (await this.prisma.room.findUnique({ await this.roomsService.updateRoomPack(payload.roomId, questionsArray);
where: { id: payload.roomId },
include: { roomPack: true } as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) { // После обновления вопросов проверяем и обновляем currentQuestionId
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[]; const room = (await this.prisma.room.findUnique({
const currentQuestionId = (room.currentQuestionId as string | null) || null; where: { id: payload.roomId },
include: { roomPack: true } as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
// Проверяем, что currentQuestionId все еще валиден if (room) {
let validQuestionId = currentQuestionId; const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
if (currentQuestionId) { const currentQuestionId = (room.currentQuestionId as string | null) || null;
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
if (!questionExists) { // Проверяем, что currentQuestionId все еще валиден
// Текущий вопрос был удален, устанавливаем первый let validQuestionId = currentQuestionId;
validQuestionId = questions[0]?.id && typeof questions[0].id === 'string' 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 ? questions[0].id
: null; : null;
} }
} else if (questions.length > 0) {
// Если нет currentQuestionId, устанавливаем первый // Обновляем currentQuestionId если изменился
validQuestionId = questions[0].id && typeof questions[0].id === 'string' if (validQuestionId !== currentQuestionId) {
? questions[0].id const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId);
: null; await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: validQuestionId,
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
}
});
}
} }
// Обновляем currentQuestionId если изменился await this.broadcastFullState(payload.roomCode);
if (validQuestionId !== currentQuestionId) { } catch (error: any) {
const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId); console.error('Error updating room pack:', error);
await this.prisma.room.update({ client.emit('error', { message: error.message || 'Failed to update questions' });
where: { id: payload.roomId },
data: {
currentQuestionId: validQuestionId,
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
}
});
}
} }
await this.broadcastFullState(payload.roomCode);
} }
@SubscribeMessage('importQuestions') @SubscribeMessage('importQuestions')
@ -810,8 +818,13 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
return; return;
} }
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices); try {
await this.broadcastFullState(payload.roomCode); 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') @SubscribeMessage('kickPlayer')

View file

@ -1,6 +1,8 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { questionsApi } from '../services/api' import { questionsApi } from '../services/api'
import { useTheme } from '../context/ThemeContext' import { useTheme } from '../context/ThemeContext'
import { useAuth } from '../context/AuthContext'
import socketService from '../services/socket'
import './GameManagementModal.css' import './GameManagementModal.css'
import './QuestionsModal.css' import './QuestionsModal.css'
@ -37,6 +39,7 @@ const GameManagementModal = ({
onAddPlayer, onAddPlayer,
}) => { }) => {
const { currentThemeData } = useTheme() const { currentThemeData } = useTheme()
const { user } = useAuth()
const [activeTab, setActiveTab] = useState(initialTab) // players | game | scoring | questions const [activeTab, setActiveTab] = useState(initialTab) // players | game | scoring | questions
const [selectedPlayer, setSelectedPlayer] = useState(null) const [selectedPlayer, setSelectedPlayer] = useState(null)
const [customPoints, setCustomPoints] = useState(10) const [customPoints, setCustomPoints] = useState(10)
@ -96,6 +99,24 @@ const GameManagementModal = ({
} }
}, [isOpen, initialTab]) }, [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 if (!isOpen) return null
const gameStatus = room?.status || 'WAITING' const gameStatus = room?.status || 'WAITING'
@ -256,13 +277,16 @@ const GameManagementModal = ({
})), })),
} }
// Убеждаемся, что questions - это массив
const currentQuestions = Array.isArray(questions) ? questions : []
let updatedQuestions let updatedQuestions
if (editingQuestion) { if (editingQuestion) {
updatedQuestions = questions.map(q => updatedQuestions = currentQuestions.map(q =>
q.id === editingQuestion.id ? questionData : q q.id === editingQuestion.id ? questionData : q
) )
} else { } else {
updatedQuestions = [...questions, questionData] updatedQuestions = [...currentQuestions, questionData]
} }
onUpdateQuestions(updatedQuestions) onUpdateQuestions(updatedQuestions)
@ -577,27 +601,35 @@ const GameManagementModal = ({
} }
const handleImportSelected = () => { const handleImportSelected = () => {
if (!room?.id || !room?.code || !user?.id || !selectedPack) {
setJsonError('Ошибка: недостаточно данных для импорта')
return
}
const indices = Array.from(selectedQuestionIndices) 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) => ({ try {
id: crypto.randomUUID(), socketService.importQuestions(
text: q.text || '', room.id,
answers: (q.answers || []).map(a => ({ room.code,
id: a.id || crypto.randomUUID(), user.id,
text: a.text, selectedPack,
points: a.points indices
})), )
}))
const updatedQuestions = [...questions, ...copiedQuestions] setSelectedQuestionIndices(new Set())
onUpdateQuestions(updatedQuestions) setSearchQuery('')
setShowPackImportModal(false)
setSelectedQuestionIndices(new Set()) setJsonError('')
setSearchQuery('') // Уведомление будет показано через WebSocket событие gameStateUpdated
setShowPackImportModal(false) } catch (error) {
setJsonError('') console.error('Error importing questions:', error)
alert(`Импортировано ${copiedQuestions.length} вопросов`) setJsonError('Ошибка при импорте вопросов: ' + (error.message || 'Неизвестная ошибка'))
}
} }
return ( return (

View file

@ -46,7 +46,9 @@ const GamePage = () => {
// Храним participantId текущего пользователя для проверки удаления // Храним participantId текущего пользователя для проверки удаления
const currentUserParticipantIdRef = useRef(null); const currentUserParticipantIdRef = useRef(null);
// Храним предыдущий themeId комнаты для отслеживания изменений // Храним предыдущий themeId комнаты для отслеживания изменений
const previousThemeIdRef = useRef(null); // Используем undefined для обозначения первой загрузки (не установлено),
// null означает "тема не установлена в комнате"
const previousThemeIdRef = useRef(undefined);
// ЕДИНСТВЕННЫЙ обработчик состояния игры // ЕДИНСТВЕННЫЙ обработчик состояния игры
useEffect(() => { useEffect(() => {
@ -65,13 +67,14 @@ const GamePage = () => {
currentUserParticipantIdRef.current = currentUserParticipant?.id || null; currentUserParticipantIdRef.current = currentUserParticipant?.id || null;
} }
// Применяем тему комнаты, если она изменилась // Применяем тему комнаты при первой загрузке или если она изменилась
const currentThemeId = state.themeId || null; const currentThemeId = state.themeId || null;
if (currentThemeId !== previousThemeIdRef.current) { const isFirstLoad = previousThemeIdRef.current === undefined;
if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId; previousThemeIdRef.current = currentThemeId;
if (currentThemeId) { // Применяем тему (даже если null - вернемся к дефолтной теме)
changeTheme(currentThemeId); changeTheme(currentThemeId);
}
} }
}; };
@ -268,14 +271,17 @@ const GamePage = () => {
}; };
const handleUpdateRoomQuestions = async (newQuestions) => { const handleUpdateRoomQuestions = async (newQuestions) => {
if (!gameState.roomId) return; if (!gameState.roomId || !user?.id) return;
try { try {
// Убеждаемся, что newQuestions - это массив
const questionsArray = Array.isArray(newQuestions) ? newQuestions : [];
socketService.emit('updateRoomPack', { socketService.emit('updateRoomPack', {
roomId: gameState.roomId, roomId: gameState.roomId,
roomCode: gameState.roomCode, roomCode: gameState.roomCode,
userId: user.id, userId: user.id,
questions: newQuestions questions: questionsArray
}); });
} catch (error) { } catch (error) {
console.error('Error updating room pack:', error); console.error('Error updating room pack:', error);

View file

@ -19,7 +19,8 @@ const RoomPage = () => {
const { changeTheme } = useTheme(); const { changeTheme } = useTheme();
// Храним предыдущий themeId комнаты для отслеживания изменений // Храним предыдущий themeId комнаты для отслеживания изменений
const previousThemeIdRef = useRef(null); // Используем undefined для обозначения первой загрузки (не установлено)
const previousThemeIdRef = useRef(undefined);
// Callback для автоматической навигации при старте игры // Callback для автоматической навигации при старте игры
const handleGameStartedEvent = useCallback(() => { 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(() => { useEffect(() => {
const handleJoin = async () => { const handleJoin = async () => {
if (room && user && !joined && !isRoleSelectionModalOpen) { // Пропускаем если нет комнаты, пользователя или уже присоединились
const isParticipant = participants.some((p) => p.userId === user.id); if (!room || !user || joined) return;
if (!isParticipant) {
// Если зрители не разрешены, присоединяемся как PLAYER автоматически // Проверяем, не является ли пользователь уже участником
// Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED) const isParticipant = participants.some((p) => p.userId === user.id);
if (!room.allowSpectators) { if (isParticipant) {
try { setJoined(true);
setJoinError(null); return;
await joinRoom(room.id, user.id, user.name || 'Гость', 'PLAYER'); }
setJoined(true);
// Если игра уже началась, перенаправление произойдет после обновления participants // Если зрители разрешены, показываем модальное окно выбора роли
} catch (error) { if (room.allowSpectators) {
console.error('Join error:', error); if (!isRoleSelectionModalOpen) {
const errorMessage = error.response?.data?.message || error.message || 'Ошибка при присоединении к комнате'; setIsRoleSelectionModalOpen(true);
setJoinError(errorMessage);
alert(errorMessage);
}
}
} else {
setJoined(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 handleGameStateUpdated = (state) => {
const currentThemeId = state.themeId || null; const currentThemeId = state.themeId || null;
// Применяем тему если она изменилась или если это первое присоединение (previousThemeIdRef.current === null) const isFirstLoad = previousThemeIdRef.current === undefined;
if (currentThemeId !== previousThemeIdRef.current) {
// Применяем тему при первой загрузке или если она изменилась
if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId; previousThemeIdRef.current = currentThemeId;
if (currentThemeId) { // Применяем тему (даже если null - вернемся к дефолтной теме)
changeTheme(currentThemeId); changeTheme(currentThemeId);
}
} }
}; };
@ -248,9 +245,12 @@ const RoomPage = () => {
if (!roomCode || !room) return; if (!roomCode || !room) return;
const currentThemeId = room.themeId || null; const currentThemeId = room.themeId || null;
// Применяем тему если она существует и еще не применена (первое присоединение) или изменилась const isFirstLoad = previousThemeIdRef.current === undefined;
if (currentThemeId && currentThemeId !== previousThemeIdRef.current) {
// Применяем тему при первой загрузке или если она изменилась
if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId; previousThemeIdRef.current = currentThemeId;
// Применяем тему (даже если null - вернемся к дефолтной теме)
changeTheme(currentThemeId); changeTheme(currentThemeId);
} }
}, [roomCode, room?.themeId, changeTheme]); }, [roomCode, room?.themeId, changeTheme]);