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;
}
// Обновляем вопросы через 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')

View file

@ -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 (

View file

@ -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);

View file

@ -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]);