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

467 lines
16 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 комнаты для отслеживания изменений
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;