stiff
This commit is contained in:
parent
403ea8ac24
commit
06e95fb432
11 changed files with 186 additions and 226 deletions
|
|
@ -29,12 +29,11 @@ export interface ThemeSettings {
|
|||
particleSymbol?: string
|
||||
particleColor?: string
|
||||
particleGlow?: string
|
||||
particleTargetCount?: number
|
||||
particleSpawnCount?: number
|
||||
particleUpdateInterval?: number
|
||||
particleDurationMin?: number
|
||||
particleDurationMax?: number
|
||||
particleInitialDelayMax?: number
|
||||
particleDensity?: number
|
||||
// Finish Screen Settings
|
||||
finishScreenTitle?: string
|
||||
finishScreenSubtitle?: string
|
||||
|
|
@ -313,12 +312,11 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
|
|||
particleSymbol: '❄',
|
||||
particleColor: '#ffffff',
|
||||
particleGlow: 'rgba(255, 255, 255, 0.8)',
|
||||
particleTargetCount: 200,
|
||||
particleSpawnCount: 0.1,
|
||||
particleUpdateInterval: 1000,
|
||||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
particleInitialDelayMax: 10,
|
||||
particleDensity: 100,
|
||||
finishScreenTitle: 'Игра завершена!',
|
||||
finishScreenSubtitle: '',
|
||||
finishScreenBgColor: 'rgba(0, 0, 0, 0.5)',
|
||||
|
|
|
|||
|
|
@ -598,46 +598,25 @@ export function ThemeEditorDialog({
|
|||
<h4 className="text-md font-medium">Animation Settings (Настройки анимации)</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="particleTargetCount">
|
||||
Target Count (Целевое количество)
|
||||
<Label htmlFor="particleSpawnCount">
|
||||
Spawn Count (частиц/пиксель ширины)
|
||||
</Label>
|
||||
<Input
|
||||
id="particleTargetCount"
|
||||
id="particleSpawnCount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1000"
|
||||
value={settings.particleTargetCount ?? DEFAULT_THEME_SETTINGS.particleTargetCount ?? 200}
|
||||
min="0.01"
|
||||
max="10"
|
||||
step="0.01"
|
||||
value={settings.particleSpawnCount ?? DEFAULT_THEME_SETTINGS.particleSpawnCount ?? 0.1}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
const value = parseFloat(e.target.value)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateSetting('particleTargetCount', value)
|
||||
updateSetting('particleSpawnCount', value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Целевое количество снежинок на экране (используется, если плотность не задана)
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="particleDensity">
|
||||
Density (Плотность, частиц/Мп)
|
||||
</Label>
|
||||
<Input
|
||||
id="particleDensity"
|
||||
type="number"
|
||||
min="1"
|
||||
max="500"
|
||||
step="1"
|
||||
value={settings.particleDensity ?? DEFAULT_THEME_SETTINGS.particleDensity ?? 100}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!isNaN(value) && value > 0) {
|
||||
updateSetting('particleDensity', value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Плотность частиц (количество на 1Мп площади экрана). Учитывает размер экрана для одинаковой визуальной плотности на разных устройствах
|
||||
Количество частиц на единицу ширины экрана. Обеспечивает одинаковую визуальную плотность на разных устройствах
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ async function main() {
|
|||
particleSymbol: '❄',
|
||||
particleColor: '#ffffff',
|
||||
particleGlow: 'rgba(255, 215, 0, 0.8)',
|
||||
particleTargetCount: 200,
|
||||
particleSpawnCount: 0.1,
|
||||
particleUpdateInterval: 1000,
|
||||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
|
|
@ -327,7 +327,7 @@ async function main() {
|
|||
particleSymbol: '🌸',
|
||||
particleColor: '#2d3748',
|
||||
particleGlow: 'rgba(47, 128, 237, 0.6)',
|
||||
particleTargetCount: 200,
|
||||
particleSpawnCount: 0.1,
|
||||
particleUpdateInterval: 1000,
|
||||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
|
|
@ -367,7 +367,7 @@ async function main() {
|
|||
particleSymbol: '🎉',
|
||||
particleColor: '#ffffff',
|
||||
particleGlow: 'rgba(255, 87, 108, 0.8)',
|
||||
particleTargetCount: 200,
|
||||
particleSpawnCount: 0.1,
|
||||
particleUpdateInterval: 1000,
|
||||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
|
|
@ -407,7 +407,7 @@ async function main() {
|
|||
particleSymbol: '✨',
|
||||
particleColor: '#e0e0e0',
|
||||
particleGlow: 'rgba(100, 255, 218, 0.6)',
|
||||
particleTargetCount: 200,
|
||||
particleSpawnCount: 0.1,
|
||||
particleUpdateInterval: 1000,
|
||||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export class ThemeSettingsDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
particleTargetCount?: number;
|
||||
particleSpawnCount?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
|
|
@ -112,11 +112,6 @@ export class ThemeSettingsDto {
|
|||
@Type(() => Number)
|
||||
particleInitialDelayMax?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
particleDensity?: number;
|
||||
|
||||
// Finish Screen Settings
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
|
||||
@SubscribeMessage('joinRoom')
|
||||
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
|
||||
console.log(`🔌 Client ${client.id} joining WebSocket room ${payload.roomCode}, userId: ${payload.userId}`);
|
||||
client.join(payload.roomCode);
|
||||
|
||||
// Получаем полное состояние для отправки присоединившемуся клиенту
|
||||
|
|
@ -573,6 +574,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
})
|
||||
};
|
||||
|
||||
console.log(`📡 Broadcasting gameStateUpdated to room ${roomCode} with ${room.participants.length} participants`);
|
||||
this.server.to(roomCode).emit('gameStateUpdated', fullState);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ export class RoomsService {
|
|||
// WebSocket joinRoom может выполняться параллельно с REST API joinRoom
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
console.log(`📤 Broadcasting room update for ${updatedRoom.code} with ${updatedRoom.participants.length} participants`);
|
||||
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
||||
// Также отправляем gameStateUpdated через broadcastFullState
|
||||
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
|||
import { useTheme } from '../context/ThemeContext'
|
||||
|
||||
// Default values for particle animation settings
|
||||
const DEFAULT_TARGET_COUNT = 200
|
||||
const DEFAULT_SPAWN_COUNT = 0.1
|
||||
const DEFAULT_UPDATE_INTERVAL = 1000
|
||||
const DEFAULT_DURATION_MIN = 7
|
||||
const DEFAULT_DURATION_MAX = 10
|
||||
|
|
@ -53,29 +53,16 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
return currentThemeData?.settings?.particleSymbol || '❄'
|
||||
}
|
||||
|
||||
// Get particle density from theme settings
|
||||
const getParticleDensity = () => {
|
||||
return currentThemeData?.settings?.particleDensity
|
||||
// Get particle spawn count from theme settings (particles per pixel width)
|
||||
const getParticleSpawnCount = () => {
|
||||
return currentThemeData?.settings?.particleSpawnCount ?? DEFAULT_SPAWN_COUNT
|
||||
}
|
||||
|
||||
// Calculate target count based on density and screen size, or use fixed target count
|
||||
// Calculate target count based on spawn count and screen width
|
||||
const calculateTargetCount = () => {
|
||||
const density = getParticleDensity()
|
||||
|
||||
// If density is set, calculate based on screen area (particles per 1Mp)
|
||||
if (density !== undefined && density > 0) {
|
||||
const area = windowSize.width * windowSize.height
|
||||
const count = Math.round((density * area) / 1000000)
|
||||
return Math.max(1, count) // Ensure at least 1 particle
|
||||
}
|
||||
|
||||
// Fallback to fixed target count
|
||||
return currentThemeData?.settings?.particleTargetCount ?? DEFAULT_TARGET_COUNT
|
||||
}
|
||||
|
||||
// Get particle animation settings from theme with defaults
|
||||
const getParticleTargetCount = () => {
|
||||
return calculateTargetCount()
|
||||
const spawnCount = getParticleSpawnCount()
|
||||
const count = Math.round(spawnCount * windowSize.width)
|
||||
return Math.max(1, count) // Ensure at least 1 particle
|
||||
}
|
||||
|
||||
const getParticleUpdateInterval = () => {
|
||||
|
|
@ -104,7 +91,7 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
|
||||
const particlesEnabled = getParticlesEnabled()
|
||||
const particleSymbol = getParticleSymbol()
|
||||
const targetCount = getParticleTargetCount()
|
||||
const targetCount = calculateTargetCount()
|
||||
const updateInterval = getParticleUpdateInterval()
|
||||
const durationRange = getParticleDurationRange()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,23 @@ export const ThemeProvider = ({ children }) => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [pendingThemeId, setPendingThemeId] = useState(null);
|
||||
const [currentTheme, setCurrentTheme] = useState(() => {
|
||||
// Загружаем полный объект темы из localStorage
|
||||
const saved = localStorage.getItem('app-theme');
|
||||
return saved || null;
|
||||
if (saved) {
|
||||
try {
|
||||
const themeData = JSON.parse(saved);
|
||||
// Если это объект темы, возвращаем его ID
|
||||
if (themeData && typeof themeData === 'object' && themeData.id) {
|
||||
return themeData.id;
|
||||
}
|
||||
// Если это просто строка ID (старый формат), возвращаем её
|
||||
return themeData;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse theme from localStorage:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Load themes from API
|
||||
|
|
@ -38,9 +53,16 @@ export const ThemeProvider = ({ children }) => {
|
|||
// Find theme marked as default, or use first theme as fallback
|
||||
const defaultTheme = data.find((t) => t.isDefault === true) || data[0];
|
||||
if (defaultTheme) {
|
||||
localStorage.setItem('app-theme', defaultTheme.id);
|
||||
// Сохраняем полный объект темы
|
||||
localStorage.setItem('app-theme', JSON.stringify(defaultTheme));
|
||||
return defaultTheme.id;
|
||||
}
|
||||
} else {
|
||||
// Обновляем сохраненную тему актуальными данными с сервера
|
||||
const currentThemeData = data.find((t) => t.id === prevTheme);
|
||||
if (currentThemeData) {
|
||||
localStorage.setItem('app-theme', JSON.stringify(currentThemeData));
|
||||
}
|
||||
}
|
||||
return prevTheme;
|
||||
});
|
||||
|
|
@ -69,11 +91,38 @@ export const ThemeProvider = ({ children }) => {
|
|||
|
||||
// Apply theme when currentTheme or themes change
|
||||
useEffect(() => {
|
||||
if (!currentTheme || themes.length === 0) return;
|
||||
// Сначала пытаемся применить тему из localStorage для мгновенного отображения
|
||||
const applyThemeFromCache = () => {
|
||||
const savedThemeStr = localStorage.getItem('app-theme');
|
||||
if (savedThemeStr) {
|
||||
try {
|
||||
const savedTheme = JSON.parse(savedThemeStr);
|
||||
if (savedTheme && typeof savedTheme === 'object' && savedTheme.colors && savedTheme.settings) {
|
||||
applyThemeStyles(savedTheme);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to apply cached theme:', e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const theme = themes.find((t) => t.id === currentTheme);
|
||||
if (!theme) return;
|
||||
// Применяем кэшированную тему сразу
|
||||
const cacheApplied = applyThemeFromCache();
|
||||
|
||||
// Затем применяем актуальную тему из загруженного списка (если она отличается)
|
||||
if (currentTheme && themes.length > 0) {
|
||||
const theme = themes.find((t) => t.id === currentTheme);
|
||||
if (theme) {
|
||||
applyThemeStyles(theme);
|
||||
// Сохраняем полный объект темы
|
||||
localStorage.setItem('app-theme', JSON.stringify(theme));
|
||||
}
|
||||
}
|
||||
}, [currentTheme, themes]);
|
||||
|
||||
const applyThemeStyles = (theme) => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Remove data-theme attribute (for built-in CSS themes)
|
||||
|
|
@ -92,7 +141,7 @@ export const ThemeProvider = ({ children }) => {
|
|||
// Skip boolean and number values - they are handled separately (numbers for JS, booleans for logic)
|
||||
// Only string values are used as CSS variables (except particle animation numbers)
|
||||
const isParticleNumber = [
|
||||
'particleTargetCount',
|
||||
'particleSpawnCount',
|
||||
'particleUpdateInterval',
|
||||
'particleDurationMin',
|
||||
'particleDurationMax',
|
||||
|
|
@ -117,9 +166,7 @@ export const ThemeProvider = ({ children }) => {
|
|||
root.style.setProperty('--particle-color', particleColor);
|
||||
root.style.setProperty('--particle-glow', particleGlow);
|
||||
}
|
||||
|
||||
localStorage.setItem('app-theme', currentTheme);
|
||||
}, [currentTheme, themes]);
|
||||
};
|
||||
|
||||
const changeTheme = (themeId) => {
|
||||
// Если темы еще не загружены, сохраняем themeId для применения после загрузки
|
||||
|
|
@ -129,8 +176,11 @@ export const ThemeProvider = ({ children }) => {
|
|||
}
|
||||
|
||||
// Если тема существует в списке, применяем её
|
||||
if (themes.find((t) => t.id === themeId)) {
|
||||
const theme = themes.find((t) => t.id === themeId);
|
||||
if (theme) {
|
||||
setCurrentTheme(themeId);
|
||||
// Сохраняем полный объект темы
|
||||
localStorage.setItem('app-theme', JSON.stringify(theme));
|
||||
setPendingThemeId(null);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,19 +4,83 @@ import socketService from '../services/socket';
|
|||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||
const { user } = useAuth();
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const [room, setRoom] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [participants, setParticipants] = useState([]);
|
||||
const [requiresPassword, setRequiresPassword] = useState(false);
|
||||
|
||||
// Обработчики событий вынесены наружу useEffect, чтобы они регистрировались только один раз
|
||||
// и не зависели от изменений зависимостей
|
||||
const handleRoomUpdate = useCallback((updatedRoom) => {
|
||||
console.log('📨 roomUpdate received:', updatedRoom.participants?.length, 'participants');
|
||||
setRoom(updatedRoom);
|
||||
setParticipants(updatedRoom.participants || []);
|
||||
}, []);
|
||||
|
||||
const handleGameStarted = useCallback((updatedRoom) => {
|
||||
console.log('🎮 gameStarted received');
|
||||
setRoom(updatedRoom);
|
||||
// Вызываем callback для навигации на страницу игры
|
||||
if (onGameStarted) {
|
||||
onGameStarted(updatedRoom);
|
||||
}
|
||||
}, [onGameStarted]);
|
||||
|
||||
const handleGameStateUpdated = useCallback((state) => {
|
||||
console.log('🔄 gameStateUpdated received:', state.participants?.length, 'participants');
|
||||
// Обновляем только базовую информацию о комнате
|
||||
// Полное состояние игры управляется в GamePage
|
||||
if (state.participants) {
|
||||
setParticipants(state.participants);
|
||||
}
|
||||
|
||||
// Обновляем базовую информацию о комнате из состояния
|
||||
setRoom(prevRoom => {
|
||||
if (!prevRoom) return prevRoom;
|
||||
|
||||
const updatedRoom = { ...prevRoom };
|
||||
|
||||
if (state.status) {
|
||||
updatedRoom.status = state.status;
|
||||
}
|
||||
|
||||
if (state.themeId !== undefined) {
|
||||
updatedRoom.themeId = state.themeId;
|
||||
}
|
||||
|
||||
if (state.voiceMode !== undefined) {
|
||||
updatedRoom.voiceMode = state.voiceMode;
|
||||
}
|
||||
|
||||
return updatedRoom;
|
||||
});
|
||||
|
||||
// Если игра началась, вызываем callback
|
||||
if (state.status === 'PLAYING' && onGameStarted) {
|
||||
onGameStarted(state);
|
||||
}
|
||||
}, [onGameStarted]);
|
||||
|
||||
const handleRoomPackUpdated = useCallback((updatedRoom) => {
|
||||
setRoom(updatedRoom);
|
||||
if (updatedRoom.participants) {
|
||||
setParticipants(updatedRoom.participants);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomCode) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ждем загрузки пользователя из куки перед запросом комнаты
|
||||
if (authLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchRoom = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -36,8 +100,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
setError(null);
|
||||
setRequiresPassword(false);
|
||||
|
||||
// Сохраняем пароль, если вход успешен
|
||||
if (roomPassword) {
|
||||
// Сохраняем пароль, если вход успешен И пользователь не является хостом
|
||||
if (roomPassword && response.data.hostId !== user?.id) {
|
||||
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||
}
|
||||
|
||||
|
|
@ -64,63 +128,7 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
|
||||
fetchRoom();
|
||||
|
||||
// Listen for room updates (регистрируются всегда, но не подключаются если requiresPassword)
|
||||
const handleRoomUpdate = (updatedRoom) => {
|
||||
setRoom(updatedRoom);
|
||||
setParticipants(updatedRoom.participants || []);
|
||||
};
|
||||
|
||||
const handleGameStarted = (updatedRoom) => {
|
||||
setRoom(updatedRoom);
|
||||
// Вызываем callback для навигации на страницу игры
|
||||
if (onGameStarted) {
|
||||
onGameStarted(updatedRoom);
|
||||
}
|
||||
};
|
||||
|
||||
// Используем новое событие gameStateUpdated если нужно обновлять состояние
|
||||
const handleGameStateUpdated = (state) => {
|
||||
// Обновляем только базовую информацию о комнате
|
||||
// Полное состояние игры управляется в GamePage
|
||||
if (state.participants) {
|
||||
setParticipants(state.participants);
|
||||
}
|
||||
|
||||
// Обновляем базовую информацию о комнате из состояния
|
||||
setRoom(prevRoom => {
|
||||
if (!prevRoom) return prevRoom;
|
||||
|
||||
const updatedRoom = { ...prevRoom };
|
||||
|
||||
if (state.status) {
|
||||
updatedRoom.status = state.status;
|
||||
}
|
||||
|
||||
if (state.themeId !== undefined) {
|
||||
updatedRoom.themeId = state.themeId;
|
||||
}
|
||||
|
||||
if (state.voiceMode !== undefined) {
|
||||
updatedRoom.voiceMode = state.voiceMode;
|
||||
}
|
||||
|
||||
return updatedRoom;
|
||||
});
|
||||
|
||||
// Если игра началась, вызываем callback
|
||||
if (state.status === 'PLAYING' && onGameStarted) {
|
||||
onGameStarted(state);
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик обновления вопросов комнаты
|
||||
const handleRoomPackUpdated = (updatedRoom) => {
|
||||
setRoom(updatedRoom);
|
||||
if (updatedRoom.participants) {
|
||||
setParticipants(updatedRoom.participants);
|
||||
}
|
||||
};
|
||||
|
||||
// Регистрируем обработчики событий
|
||||
socketService.on('roomUpdate', handleRoomUpdate);
|
||||
socketService.on('gameStarted', handleGameStarted);
|
||||
socketService.on('gameStateUpdated', handleGameStateUpdated);
|
||||
|
|
@ -132,7 +140,7 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
socketService.off('gameStateUpdated', handleGameStateUpdated);
|
||||
socketService.off('roomPackUpdated', handleRoomPackUpdated);
|
||||
};
|
||||
}, [roomCode, password, onGameStarted, user?.id]);
|
||||
}, [roomCode, password, user?.id, authLoading, handleRoomUpdate, handleGameStarted, handleGameStateUpdated, handleRoomPackUpdated]);
|
||||
|
||||
const createRoom = useCallback(async (hostId, questionPackId, settings = {}, hostName) => {
|
||||
try {
|
||||
|
|
@ -203,8 +211,10 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
setError(null);
|
||||
setRequiresPassword(false);
|
||||
|
||||
// Сохраняем пароль в localStorage при успешном входе
|
||||
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||
// Сохраняем пароль в localStorage при успешном входе, НО НЕ для хоста
|
||||
if (response.data.hostId !== user?.id) {
|
||||
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||
}
|
||||
|
||||
// ✅ После успешной загрузки комнаты с паролем подключаемся к WebSocket
|
||||
socketService.connect();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useRoom } from '../hooks/useRoom';
|
||||
import NameInputModal from '../components/NameInputModal';
|
||||
|
||||
const CreateRoom = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, loginAnonymous, loading: authLoading } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { createRoom, loading: roomLoading } = useRoom();
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
|
|
@ -14,42 +13,14 @@ const CreateRoom = () => {
|
|||
allowSpectators: true,
|
||||
password: '',
|
||||
});
|
||||
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
|
||||
const [isHostNameModalOpen, setIsHostNameModalOpen] = useState(false);
|
||||
|
||||
// Проверка авторизации и показ модального окна для ввода имени
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
setIsNameModalOpen(true);
|
||||
} else if (user) {
|
||||
setIsNameModalOpen(false);
|
||||
}
|
||||
}, [authLoading, user]);
|
||||
|
||||
// Обработка ввода имени и авторизация
|
||||
const handleNameSubmit = async (name) => {
|
||||
try {
|
||||
await loginAnonymous(name);
|
||||
setIsNameModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
alert('Ошибка при авторизации. Попробуйте еще раз.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRoom = async () => {
|
||||
if (!user) {
|
||||
setIsNameModalOpen(true);
|
||||
alert('Пожалуйста, задайте имя на главном экране');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Всегда спрашиваем имя хоста перед созданием комнаты
|
||||
setIsHostNameModalOpen(true);
|
||||
};
|
||||
|
||||
const handleHostNameSubmit = async (name) => {
|
||||
setIsHostNameModalOpen(false);
|
||||
|
||||
try {
|
||||
// Очищаем пустой пароль перед отправкой
|
||||
const cleanSettings = { ...settings };
|
||||
|
|
@ -63,7 +34,7 @@ const CreateRoom = () => {
|
|||
user.id,
|
||||
undefined,
|
||||
cleanSettings,
|
||||
name.trim(),
|
||||
user.name,
|
||||
);
|
||||
navigate(`/room/${room.code}`);
|
||||
} catch (error) {
|
||||
|
|
@ -121,7 +92,7 @@ const CreateRoom = () => {
|
|||
<div className="button-group">
|
||||
<button
|
||||
onClick={handleCreateRoom}
|
||||
disabled={roomLoading}
|
||||
disabled={roomLoading || !user}
|
||||
className="primary"
|
||||
>
|
||||
{roomLoading ? 'Создание...' : 'Создать комнату'}
|
||||
|
|
@ -129,20 +100,6 @@ const CreateRoom = () => {
|
|||
<button onClick={() => navigate('/')}>Назад</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NameInputModal
|
||||
isOpen={isNameModalOpen}
|
||||
onSubmit={handleNameSubmit}
|
||||
onCancel={null}
|
||||
/>
|
||||
|
||||
<NameInputModal
|
||||
isOpen={isHostNameModalOpen}
|
||||
onSubmit={handleHostNameSubmit}
|
||||
onCancel={() => setIsHostNameModalOpen(false)}
|
||||
title="Введите ваше имя как ведущего"
|
||||
description="Чтобы создать комнату, введите ваше имя как ведущего"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ 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';
|
||||
|
|
@ -15,7 +14,7 @@ import GameManagementModal from '../components/GameManagementModal';
|
|||
const RoomPage = () => {
|
||||
const { roomCode } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { user, loginAnonymous, loading: authLoading } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { changeTheme } = useTheme();
|
||||
|
||||
// Храним предыдущий themeId комнаты для отслеживания изменений
|
||||
|
|
@ -41,7 +40,6 @@ const RoomPage = () => {
|
|||
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);
|
||||
|
|
@ -53,6 +51,11 @@ const RoomPage = () => {
|
|||
// Ref для отслеживания попытки присоединения (защита от двойного запроса)
|
||||
const joinAttemptedRef = useRef(false);
|
||||
|
||||
// Логирование изменений participants для отладки
|
||||
useEffect(() => {
|
||||
console.log('👥 RoomPage participants updated:', participants.length, participants.map(p => p.name));
|
||||
}, [participants]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateQR = async () => {
|
||||
try {
|
||||
|
|
@ -78,24 +81,12 @@ const RoomPage = () => {
|
|||
}, [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 {
|
||||
|
|
@ -113,22 +104,18 @@ const RoomPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Обработка ввода имени и авторизация
|
||||
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;
|
||||
// Перенаправляем на главный экран, если нет пользователя
|
||||
if (!user) {
|
||||
alert('Пожалуйста, задайте имя на главном экране');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Пропускаем если нет комнаты или уже присоединились
|
||||
if (!room || joined) return;
|
||||
|
||||
// Проверяем, не является ли пользователь уже участником
|
||||
const isParticipant = participants.some((p) => p.userId === user.id);
|
||||
|
|
@ -169,7 +156,7 @@ const RoomPage = () => {
|
|||
};
|
||||
|
||||
handleJoin();
|
||||
}, [room, user, participants, joined, joinRoom, isRoleSelectionModalOpen]);
|
||||
}, [room, user, joined, participants, joinRoom, navigate, isRoleSelectionModalOpen]);
|
||||
|
||||
// Обработка выбора роли
|
||||
// Присоединение разрешено независимо от статуса игры (WAITING, PLAYING, FINISHED)
|
||||
|
|
@ -437,12 +424,6 @@ const RoomPage = () => {
|
|||
roomCode={roomCode}
|
||||
/>
|
||||
|
||||
<NameInputModal
|
||||
isOpen={isNameModalOpen}
|
||||
onSubmit={handleNameSubmit}
|
||||
onCancel={null}
|
||||
/>
|
||||
|
||||
<PasswordModal
|
||||
isOpen={isPasswordModalOpen}
|
||||
onSubmit={handlePasswordSubmit}
|
||||
|
|
|
|||
Loading…
Reference in a new issue