sto-k-odnomu/src/pages/RoomPage.jsx

470 lines
16 KiB
React
Raw Normal View History

2026-01-10 20:49:42 +00:00
import React, { useEffect, useState, useCallback, useRef } from 'react';
2026-01-03 14:07:04 +00:00
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
2026-01-10 20:49:42 +00:00
import { useTheme } from '../context/ThemeContext';
2026-01-03 14:07:04 +00:00
import { useRoom } from '../hooks/useRoom';
2026-01-06 20:27:50 +00:00
import { questionsApi } from '../services/api';
2026-01-03 14:07:04 +00:00
import QRCode from 'qrcode';
2026-01-10 15:51:33 +00:00
import socketService from '../services/socket';
2026-01-06 20:12:36 +00:00
import QRModal from '../components/QRModal';
2026-01-07 13:24:30 +00:00
import NameInputModal from '../components/NameInputModal';
2026-01-10 00:18:08 +00:00
import PasswordModal from '../components/PasswordModal';
2026-01-10 15:51:33 +00:00
import RoleSelectionModal from '../components/RoleSelectionModal';
import GameManagementModal from '../components/GameManagementModal';
2026-01-03 14:07:04 +00:00
const RoomPage = () => {
const { roomCode } = useParams();
const navigate = useNavigate();
2026-01-07 13:24:30 +00:00
const { user, loginAnonymous, loading: authLoading } = useAuth();
2026-01-10 20:49:42 +00:00
const { changeTheme } = useTheme();
// Храним предыдущий themeId комнаты для отслеживания изменений
const previousThemeIdRef = useRef(null);
2026-01-07 14:50:33 +00:00
// Callback для автоматической навигации при старте игры
const handleGameStartedEvent = useCallback(() => {
navigate(`/game/${roomCode}`);
}, [navigate, roomCode]);
2026-01-10 00:18:08 +00:00
const [password, setPassword] = useState(null);
2026-01-06 20:27:50 +00:00
const {
room,
participants,
loading,
error,
2026-01-10 00:18:08 +00:00
requiresPassword,
fetchRoomWithPassword,
2026-01-06 20:27:50 +00:00
joinRoom,
startGame,
2026-01-10 00:18:08 +00:00
} = useRoom(roomCode, handleGameStartedEvent, password);
2026-01-03 14:07:04 +00:00
const [qrCode, setQrCode] = useState('');
const [joined, setJoined] = useState(false);
2026-01-06 20:12:36 +00:00
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
2026-01-07 13:24:30 +00:00
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
2026-01-10 00:18:08 +00:00
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
2026-01-10 15:51:33 +00:00
const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false);
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
2026-01-10 00:18:08 +00:00
const [passwordError, setPasswordError] = useState(null);
2026-01-10 15:51:33 +00:00
const [joinError, setJoinError] = useState(null);
const [selectedRole, setSelectedRole] = useState('PLAYER');
2026-01-06 20:27:50 +00:00
const [questionPacks, setQuestionPacks] = useState([]);
2026-01-03 14:07:04 +00:00
useEffect(() => {
const generateQR = async () => {
try {
2026-01-06 20:12:36 +00:00
// Используем абсолютный 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,
});
2026-01-03 14:07:04 +00:00
setQrCode(qr);
} catch (err) {
console.error('QR generation error:', err);
}
};
if (roomCode) {
generateQR();
}
}, [roomCode]);
2026-01-10 00:18:08 +00:00
// Проверка пароля: показываем модальное окно, если требуется пароль
2026-01-10 20:49:42 +00:00
// Показываем независимо от авторизации - пароль проверяется первым
2026-01-10 00:18:08 +00:00
useEffect(() => {
2026-01-10 20:49:42 +00:00
if (requiresPassword && !isPasswordModalOpen && !loading) {
// Показывать модальное окно пароля независимо от авторизации
2026-01-10 00:18:08 +00:00
setIsPasswordModalOpen(true);
}
2026-01-10 20:49:42 +00:00
}, [requiresPassword, isPasswordModalOpen, loading]);
2026-01-10 00:18:08 +00:00
2026-01-07 13:24:30 +00:00
// Проверка авторизации и показ модального окна для ввода имени
2026-01-10 20:49:42 +00:00
// Показывать только если НЕТ пароля - пароль приоритетнее
2026-01-07 13:24:30 +00:00
useEffect(() => {
2026-01-10 00:18:08 +00:00
if (!authLoading && !user && room && !loading && !requiresPassword) {
2026-01-07 13:24:30 +00:00
setIsNameModalOpen(true);
} else if (user) {
setIsNameModalOpen(false);
}
2026-01-10 00:18:08 +00:00
}, [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('Ошибка при проверке пароля. Попробуйте еще раз.');
}
}
};
2026-01-07 13:24:30 +00:00
// Обработка ввода имени и авторизация
const handleNameSubmit = async (name) => {
try {
await loginAnonymous(name);
setIsNameModalOpen(false);
} catch (error) {
console.error('Login error:', error);
alert('Ошибка при авторизации. Попробуйте еще раз.');
}
};
2026-01-10 15:51:33 +00:00
// Показываем модальное окно выбора роли, если 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, если зрители не разрешены
2026-01-03 14:07:04 +00:00
useEffect(() => {
const handleJoin = async () => {
2026-01-10 15:51:33 +00:00
if (room && user && !joined && !isRoleSelectionModalOpen) {
2026-01-03 14:07:04 +00:00
const isParticipant = participants.some((p) => p.userId === user.id);
if (!isParticipant) {
2026-01-10 15:51:33 +00:00
// Если зрители не разрешены, присоединяемся как 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);
}
2026-01-03 14:07:04 +00:00
}
} else {
setJoined(true);
}
}
};
handleJoin();
2026-01-10 15:51:33 +00:00
}, [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);
}
};
2026-01-03 14:07:04 +00:00
2026-01-06 20:27:50 +00:00
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);
}
}
};
2026-01-10 15:51:33 +00:00
// Проверяем роль участника для поддержки нескольких хостов
const currentUserParticipant = user && participants
? participants.find(p => p.userId === user.id)
: null;
const isHost = currentUserParticipant?.role === 'HOST';
2026-01-06 20:27:50 +00:00
2026-01-10 15:51:33 +00:00
if (room && user && isHost) {
fetchPacks();
2026-01-06 20:27:50 +00:00
}
2026-01-10 15:51:33 +00:00
}, [room, user, participants]);
2026-01-06 20:27:50 +00:00
2026-01-07 14:54:32 +00:00
// Автоматически перенаправляем на страницу игры, если игра уже началась
useEffect(() => {
if (room && room.status === 'PLAYING') {
navigate(`/game/${roomCode}`);
}
}, [room, roomCode, navigate]);
2026-01-10 20:49:42 +00:00
// Применяем тему из gameStateUpdated/roomUpdate
useEffect(() => {
if (!roomCode) return;
const handleGameStateUpdated = (state) => {
const currentThemeId = state.themeId || null;
2026-01-10 21:14:59 +00:00
// Применяем тему если она изменилась или если это первое присоединение (previousThemeIdRef.current === null)
2026-01-10 20:49:42 +00:00
if (currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId;
if (currentThemeId) {
changeTheme(currentThemeId);
}
}
};
socketService.on('gameStateUpdated', handleGameStateUpdated);
return () => {
socketService.off('gameStateUpdated', handleGameStateUpdated);
};
2026-01-10 21:14:59 +00:00
}, [roomCode, changeTheme]);
// Применяем тему из room при первом присоединении или при изменении
useEffect(() => {
if (!roomCode || !room) return;
const currentThemeId = room.themeId || null;
// Применяем тему если она существует и еще не применена (первое присоединение) или изменилась
if (currentThemeId && currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId;
changeTheme(currentThemeId);
}
}, [roomCode, room?.themeId, changeTheme]);
2026-01-10 20:49:42 +00:00
2026-01-03 14:07:04 +00:00
const handleStartGame = () => {
startGame();
navigate(`/game/${roomCode}`);
};
2026-01-10 15:51:33 +00:00
// Получаем вопросы из 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 [];
}
2026-01-06 20:27:50 +00:00
}
2026-01-10 15:51:33 +00:00
return Array.isArray(questions) ? questions : [];
2026-01-06 20:27:50 +00:00
};
2026-01-10 15:51:33 +00:00
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]
);
2026-01-06 20:27:50 +00:00
2026-01-03 14:07:04 +00:00
if (loading) {
return <div className="loading">Загрузка комнаты...</div>;
}
2026-01-10 00:18:08 +00:00
// Не показываем ошибку, если требуется пароль - покажем модальное окно
if (error && !requiresPassword && error !== 'Room password required') {
2026-01-03 14:07:04 +00:00
return (
<div className="error-page">
<h1>Ошибка</h1>
<p>{error}</p>
<button onClick={() => navigate('/')}>На главную</button>
</div>
);
}
2026-01-10 00:18:08 +00:00
if (!room && !requiresPassword && !loading) {
2026-01-03 14:07:04 +00:00
return (
<div className="error-page">
<h1>Комната не найдена</h1>
<button onClick={() => navigate('/')}>На главную</button>
</div>
);
}
2026-01-10 15:51:33 +00:00
// Проверяем роль участника для поддержки нескольких хостов
const currentUserParticipant = user && participants
? participants.find(p => p.userId === user.id)
: null;
const isHost = currentUserParticipant?.role === 'HOST';
2026-01-10 00:18:08 +00:00
// Если требуется пароль, показываем только модальное окно
if (requiresPassword && !room) {
return (
<>
<div className="loading">Загрузка комнаты...</div>
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}
onCancel={() => navigate('/')}
error={passwordError}
/>
</>
);
}
2026-01-03 14:07:04 +00:00
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>
2026-01-10 16:16:44 +00:00
{isHost && (
<div className="button-group">
2026-01-10 15:51:33 +00:00
<button
onClick={() => setIsQuestionsModalOpen(true)}
2026-01-10 16:16:44 +00:00
className="primary"
2026-01-10 15:51:33 +00:00
>
2026-01-10 16:16:44 +00:00
🎛 Настроить игру
2026-01-10 15:51:33 +00:00
</button>
2026-01-10 16:16:44 +00:00
</div>
)}
2026-01-06 20:27:50 +00:00
2026-01-03 14:07:04 +00:00
<div className="button-group">
2026-01-06 20:12:36 +00:00
<button
onClick={() => setIsQRModalOpen(true)}
className="secondary"
>
Показать QR-код
</button>
2026-01-07 13:59:18 +00:00
{isHost &&
(room.status === 'WAITING' ||
(room.status === 'PLAYING' &&
(!room.questionPack ||
room.questionPack.questionCount === 0 ||
room.currentQuestionIndex === 0))) && (
2026-01-03 14:07:04 +00:00
<button
onClick={handleStartGame}
className="primary"
>
Начать игру
</button>
)}
<button onClick={() => navigate('/')}>Покинуть комнату</button>
</div>
</div>
2026-01-06 20:12:36 +00:00
<QRModal
isOpen={isQRModalOpen}
onClose={() => setIsQRModalOpen(false)}
qrCode={qrCode}
roomCode={roomCode}
/>
2026-01-07 13:24:30 +00:00
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={null}
/>
2026-01-10 00:18:08 +00:00
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}
onCancel={() => navigate('/')}
error={passwordError}
/>
2026-01-10 15:51:33 +00:00
<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}
2026-01-10 16:16:44 +00:00
onStartGame={handleStartGame}
2026-01-10 15:51:33 +00:00
/>
)}
2026-01-03 14:07:04 +00:00
</div>
);
};
export default RoomPage;