467 lines
16 KiB
JavaScript
467 lines
16 KiB
JavaScript
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 комнаты для отслеживания изменений
|
||
const previousThemeIdRef = useRef(null);
|
||
|
||
// 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('Ошибка при авторизации. Попробуйте еще раз.');
|
||
}
|
||
};
|
||
|
||
// Показываем модальное окно выбора роли, если 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 автоматически
|
||
if (!room.allowSpectators) {
|
||
try {
|
||
setJoinError(null);
|
||
await joinRoom(room.id, user.id, user.name || 'Гость', 'PLAYER');
|
||
setJoined(true);
|
||
} catch (error) {
|
||
console.error('Join error:', error);
|
||
const errorMessage = error.response?.data?.message || error.message || 'Ошибка при присоединении к комнате';
|
||
setJoinError(errorMessage);
|
||
alert(errorMessage);
|
||
}
|
||
}
|
||
} else {
|
||
setJoined(true);
|
||
}
|
||
}
|
||
};
|
||
|
||
handleJoin();
|
||
}, [room, user, participants, joined, joinRoom, isRoleSelectionModalOpen]);
|
||
|
||
// Обработка выбора роли
|
||
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);
|
||
} 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]);
|
||
|
||
// Автоматически перенаправляем на страницу игры, если игра уже началась
|
||
useEffect(() => {
|
||
if (room && room.status === 'PLAYING') {
|
||
navigate(`/game/${roomCode}`);
|
||
}
|
||
}, [room, roomCode, navigate]);
|
||
|
||
// Применяем тему из gameStateUpdated/roomUpdate
|
||
useEffect(() => {
|
||
if (!roomCode) return;
|
||
|
||
const handleGameStateUpdated = (state) => {
|
||
const currentThemeId = state.themeId || null;
|
||
if (currentThemeId !== previousThemeIdRef.current) {
|
||
previousThemeIdRef.current = currentThemeId;
|
||
if (currentThemeId) {
|
||
changeTheme(currentThemeId);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Также проверяем тему из room при изменении
|
||
if (room?.themeId) {
|
||
const currentThemeId = room.themeId || null;
|
||
if (currentThemeId !== previousThemeIdRef.current) {
|
||
previousThemeIdRef.current = currentThemeId;
|
||
if (currentThemeId) {
|
||
changeTheme(currentThemeId);
|
||
}
|
||
}
|
||
}
|
||
|
||
socketService.on('gameStateUpdated', handleGameStateUpdated);
|
||
return () => {
|
||
socketService.off('gameStateUpdated', handleGameStateUpdated);
|
||
};
|
||
}, [roomCode, room, 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;
|