sto-k-odnomu/src/pages/RoomPage.jsx
2026-01-11 00:42:10 +03:00

479 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useCallback, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';
import { useRoom } from '../hooks/useRoom';
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';
const RoomPage = () => {
const { roomCode } = useParams();
const navigate = useNavigate();
const { user, loginAnonymous, loading: authLoading } = useAuth();
const { changeTheme } = useTheme();
// Храним предыдущий themeId комнаты для отслеживания изменений
// Используем undefined для обозначения первой загрузки (не установлено)
const previousThemeIdRef = useRef(undefined);
// Callback для автоматической навигации при старте игры
const handleGameStartedEvent = useCallback(() => {
navigate(`/game/${roomCode}`);
}, [navigate, roomCode]);
const [password, setPassword] = useState(null);
const {
room,
participants,
loading,
error,
requiresPassword,
fetchRoomWithPassword,
joinRoom,
startGame,
} = useRoom(roomCode, handleGameStartedEvent, password);
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);
const [passwordError, setPasswordError] = useState(null);
const [joinError, setJoinError] = useState(null);
const [selectedRole, setSelectedRole] = useState('PLAYER');
const [questionPacks, setQuestionPacks] = useState([]);
useEffect(() => {
const generateQR = async () => {
try {
// Используем абсолютный URL с протоколом для работы из любого приложения
const origin = window.location.origin ||
`${window.location.protocol}//${window.location.host}`;
const url = `${origin}/join-room?code=${roomCode}`;
const qr = await QRCode.toDataURL(url, {
errorCorrectionLevel: 'M',
type: 'image/png',
quality: 0.92,
margin: 1,
});
setQrCode(qr);
} catch (err) {
console.error('QR generation error:', err);
}
};
if (roomCode) {
generateQR();
}
}, [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 {
setPasswordError(null);
await fetchRoomWithPassword(enteredPassword);
setPassword(enteredPassword);
setIsPasswordModalOpen(false);
} catch (error) {
console.error('Password error:', error);
if (error.response?.status === 401) {
setPasswordError('Неверный пароль. Попробуйте еще раз.');
} else {
setPasswordError('Ошибка при проверке пароля. Попробуйте еще раз.');
}
}
};
// Обработка ввода имени и авторизация
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;
// Проверяем, не является ли пользователь уже участником
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);
}
};
handleJoin();
}, [room, user, participants, joined, joinRoom, isRoleSelectionModalOpen]);
// Обработка выбора роли
// Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED)
const handleRoleSubmit = async (role) => {
if (!room || !user) return;
try {
setJoinError(null);
setIsRoleSelectionModalOpen(false);
await joinRoom(room.id, user.id, user.name || 'Гость', role);
setSelectedRole(role);
setJoined(true);
// Если игра уже началась, перенаправление произойдет после обновления participants
} catch (error) {
console.error('Join error:', error);
const errorMessage = error.response?.data?.message || error.message || 'Ошибка при присоединении к комнате';
setJoinError(errorMessage);
alert(errorMessage);
// Открываем модальное окно снова при ошибке
setIsRoleSelectionModalOpen(true);
}
};
useEffect(() => {
const fetchPacks = async () => {
if (user) {
try {
const response = await questionsApi.getPacks(user.id);
setQuestionPacks(response.data);
} catch (error) {
console.error('Error fetching question packs:', error);
}
}
};
// Проверяем роль участника для поддержки нескольких хостов
const currentUserParticipant = user && participants
? participants.find(p => p.userId === user.id)
: null;
const isHost = currentUserParticipant?.role === 'HOST';
if (room && user && isHost) {
fetchPacks();
}
}, [room, user, participants]);
// Автоматически перенаправляем на страницу игры, если игра уже началась
// Важно: перенаправляем только если игрок уже присоединился (joined === true)
// и он есть в списке участников, чтобы избежать перенаправления до добавления игрока
useEffect(() => {
if (room && room.status === 'PLAYING' && joined && user) {
const isParticipant = participants.some((p) => p.userId === user.id);
// Перенаправляем только если игрок действительно присоединился
if (isParticipant) {
navigate(`/game/${roomCode}`);
}
}
}, [room, roomCode, navigate, joined, user, participants]);
// Применяем тему из gameStateUpdated/roomUpdate
useEffect(() => {
if (!roomCode) return;
const handleGameStateUpdated = (state) => {
const currentThemeId = state.themeId || null;
const isFirstLoad = previousThemeIdRef.current === undefined;
// Применяем тему при первой загрузке или если она изменилась
if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId;
// Применяем тему (даже если null - вернемся к дефолтной теме)
changeTheme(currentThemeId);
}
};
socketService.on('gameStateUpdated', handleGameStateUpdated);
return () => {
socketService.off('gameStateUpdated', handleGameStateUpdated);
};
}, [roomCode, changeTheme]);
// Применяем тему из room при первом присоединении или при изменении
useEffect(() => {
if (!roomCode || !room) return;
const currentThemeId = room.themeId || null;
const isFirstLoad = previousThemeIdRef.current === undefined;
// Применяем тему при первой загрузке или если она изменилась
if (isFirstLoad || currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId;
// Применяем тему (даже если null - вернемся к дефолтной теме)
changeTheme(currentThemeId);
}
}, [roomCode, room?.themeId, changeTheme]);
const handleStartGame = () => {
startGame();
navigate(`/game/${roomCode}`);
};
// Получаем вопросы из roomPack (может быть JSON строкой или массивом)
const getRoomQuestions = () => {
if (!room?.roomPack?.questions) return [];
const questions = room.roomPack.questions;
if (typeof questions === 'string') {
try {
return JSON.parse(questions);
} catch {
return [];
}
}
return Array.isArray(questions) ? questions : [];
};
const roomQuestions = getRoomQuestions();
// Обновление вопросов через WebSocket
const handleUpdateRoomQuestions = useCallback(
async (newQuestions) => {
if (!room?.id || !user) return;
try {
socketService.updateRoomPack(
room.id,
room.code,
user.id,
newQuestions
);
} catch (error) {
console.error('Error updating room questions:', error);
alert('Ошибка при сохранении вопросов');
}
},
[room, user]
);
// Изменение роли участника через WebSocket
const handleChangeParticipantRole = useCallback(
(participantId, newRole) => {
if (!room?.id || !user) return;
try {
socketService.changeParticipantRole(
room.id,
room.code,
user.id,
participantId,
newRole
);
} catch (error) {
console.error('Error changing participant role:', error);
alert('Ошибка при изменении роли участника');
}
},
[room, user]
);
if (loading) {
return <div className="loading">Загрузка комнаты...</div>;
}
// Не показываем ошибку, если требуется пароль - покажем модальное окно
if (error && !requiresPassword && error !== 'Room password required') {
return (
<div className="error-page">
<h1>Ошибка</h1>
<p>{error}</p>
<button onClick={() => navigate('/')}>На главную</button>
</div>
);
}
if (!room && !requiresPassword && !loading) {
return (
<div className="error-page">
<h1>Комната не найдена</h1>
<button onClick={() => navigate('/')}>На главную</button>
</div>
);
}
// Проверяем роль участника для поддержки нескольких хостов
const currentUserParticipant = user && participants
? participants.find(p => p.userId === user.id)
: null;
const isHost = currentUserParticipant?.role === 'HOST';
// Если требуется пароль, показываем только модальное окно
if (requiresPassword && !room) {
return (
<>
<div className="loading">Загрузка комнаты...</div>
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}
onCancel={() => navigate('/')}
error={passwordError}
/>
</>
);
}
return (
<div className="room-page">
<div className="room-container">
<h1>Комната: {room.code}</h1>
<div className="room-info">
<p>Статус: {room.status === 'WAITING' ? 'Ожидание игроков' : room.status}</p>
<p>Игроков: {participants.length}/{room.maxPlayers}</p>
</div>
<div className="participants-list">
<h3>Участники:</h3>
<ul>
{participants.map((participant) => (
<li key={participant.id}>
{participant.name} {participant.role === 'HOST' && '(Ведущий)'}
{participant.role === 'SPECTATOR' && '(Зритель)'}
</li>
))}
</ul>
</div>
{isHost && (
<div className="button-group">
<button
onClick={() => setIsQuestionsModalOpen(true)}
className="primary"
>
🎛 Настроить игру
</button>
</div>
)}
<div className="button-group">
<button
onClick={() => setIsQRModalOpen(true)}
className="secondary"
>
Показать QR-код
</button>
{isHost &&
(room.status === 'WAITING' ||
(room.status === 'PLAYING' &&
(!room.questionPack ||
room.questionPack.questionCount === 0 ||
room.currentQuestionIndex === 0))) && (
<button
onClick={handleStartGame}
className="primary"
>
Начать игру
</button>
)}
<button onClick={() => navigate('/')}>Покинуть комнату</button>
</div>
</div>
<QRModal
isOpen={isQRModalOpen}
onClose={() => setIsQRModalOpen(false)}
qrCode={qrCode}
roomCode={roomCode}
/>
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={null}
/>
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}
onCancel={() => navigate('/')}
error={passwordError}
/>
<RoleSelectionModal
isOpen={isRoleSelectionModalOpen}
onSubmit={handleRoleSubmit}
onCancel={() => navigate('/')}
allowSpectators={room?.allowSpectators}
title="Выберите роль"
description={room?.allowSpectators
? "Выберите роль для присоединения к комнате"
: "Присоединиться как игрок"}
/>
{isHost && room && (
<GameManagementModal
isOpen={isQuestionsModalOpen}
onClose={() => setIsQuestionsModalOpen(false)}
initialTab="questions"
room={{
id: room.id,
code: room.code,
status: room.status,
hostId: room.hostId,
}}
participants={participants}
currentQuestion={null}
currentQuestionIndex={0}
totalQuestions={roomQuestions.length}
revealedAnswers={[]}
questions={roomQuestions}
onUpdateQuestions={handleUpdateRoomQuestions}
availablePacks={questionPacks}
onChangeParticipantRole={handleChangeParticipantRole}
onStartGame={handleStartGame}
/>
)}
</div>
);
};
export default RoomPage;