fixes and cookies
This commit is contained in:
parent
35c5fb8cd8
commit
403ea8ac24
12 changed files with 347 additions and 38 deletions
|
|
@ -34,6 +34,7 @@ export interface ThemeSettings {
|
||||||
particleDurationMin?: number
|
particleDurationMin?: number
|
||||||
particleDurationMax?: number
|
particleDurationMax?: number
|
||||||
particleInitialDelayMax?: number
|
particleInitialDelayMax?: number
|
||||||
|
particleDensity?: number
|
||||||
// Finish Screen Settings
|
// Finish Screen Settings
|
||||||
finishScreenTitle?: string
|
finishScreenTitle?: string
|
||||||
finishScreenSubtitle?: string
|
finishScreenSubtitle?: string
|
||||||
|
|
@ -317,6 +318,7 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
|
||||||
particleDurationMin: 7,
|
particleDurationMin: 7,
|
||||||
particleDurationMax: 10,
|
particleDurationMax: 10,
|
||||||
particleInitialDelayMax: 10,
|
particleInitialDelayMax: 10,
|
||||||
|
particleDensity: 100,
|
||||||
finishScreenTitle: 'Игра завершена!',
|
finishScreenTitle: 'Игра завершена!',
|
||||||
finishScreenSubtitle: '',
|
finishScreenSubtitle: '',
|
||||||
finishScreenBgColor: 'rgba(0, 0, 0, 0.5)',
|
finishScreenBgColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,29 @@ export function ThemeEditorDialog({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,11 @@ export class ThemeSettingsDto {
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
particleInitialDelayMax?: number;
|
particleInitialDelayMax?: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
particleDensity?: number;
|
||||||
|
|
||||||
// Finish Screen Settings
|
// Finish Screen Settings
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { Controller, Post, Body } from '@nestjs/common';
|
import { Controller, Post, Body, Patch, Headers, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private authService: AuthService) {}
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('anonymous')
|
@Post('anonymous')
|
||||||
async createAnonymous(@Body('name') name?: string) {
|
async createAnonymous(@Body('name') name?: string) {
|
||||||
|
|
@ -19,4 +23,34 @@ export class AuthController {
|
||||||
async login(@Body() dto: { email: string; password: string }) {
|
async login(@Body() dto: { email: string; password: string }) {
|
||||||
return this.authService.login(dto.email, dto.password);
|
return this.authService.login(dto.email, dto.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('user')
|
||||||
|
async updateUserName(
|
||||||
|
@Headers('authorization') authorization: string,
|
||||||
|
@Body('name') name: string,
|
||||||
|
) {
|
||||||
|
if (!authorization) {
|
||||||
|
throw new UnauthorizedException('Authorization header is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authorization.replace('Bearer ', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verify(token);
|
||||||
|
const userId = payload.sub;
|
||||||
|
|
||||||
|
if (!name || name.trim().length === 0) {
|
||||||
|
throw new Error('Name cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.trim().length > 50) {
|
||||||
|
throw new Error('Name is too long (max 50 characters)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.authService.updateUserName(userId, name.trim());
|
||||||
|
return { user };
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Invalid token');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,30 @@ export class AuthService {
|
||||||
async validateUser(userId: string) {
|
async validateUser(userId: string) {
|
||||||
return this.prisma.user.findUnique({ where: { id: userId } });
|
return this.prisma.user.findUnique({ where: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUserName(userId: string, name: string) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { name },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
telegramId: true,
|
||||||
|
createdAt: true,
|
||||||
|
gamesPlayed: true,
|
||||||
|
gamesWon: true,
|
||||||
|
totalPoints: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,10 @@ export class RoomsService {
|
||||||
|
|
||||||
// Отправляем событие roomUpdate всем клиентам в комнате
|
// Отправляем событие roomUpdate всем клиентам в комнате
|
||||||
if (updatedRoom) {
|
if (updatedRoom) {
|
||||||
|
// Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате
|
||||||
|
// WebSocket joinRoom может выполняться параллельно с REST API joinRoom
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
||||||
// Также отправляем gameStateUpdated через broadcastFullState
|
// Также отправляем gameStateUpdated через broadcastFullState
|
||||||
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
||||||
|
|
@ -243,6 +247,9 @@ export class RoomsService {
|
||||||
|
|
||||||
// Отправляем событие roomUpdate всем клиентам в комнате
|
// Отправляем событие roomUpdate всем клиентам в комнате
|
||||||
if (updatedRoom) {
|
if (updatedRoom) {
|
||||||
|
// Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
||||||
// Также отправляем gameStateUpdated через broadcastFullState
|
// Также отправляем gameStateUpdated через broadcastFullState
|
||||||
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,11 @@ const DEFAULT_TARGET_COUNT = 200
|
||||||
const DEFAULT_UPDATE_INTERVAL = 1000
|
const DEFAULT_UPDATE_INTERVAL = 1000
|
||||||
const DEFAULT_DURATION_MIN = 7
|
const DEFAULT_DURATION_MIN = 7
|
||||||
const DEFAULT_DURATION_MAX = 10
|
const DEFAULT_DURATION_MAX = 10
|
||||||
const DEFAULT_INITIAL_DELAY_MAX = 10
|
|
||||||
|
|
||||||
function createSnowflake(id, options = {}) {
|
function createSnowflake(id, options = {}) {
|
||||||
const {
|
const {
|
||||||
durationMin = DEFAULT_DURATION_MIN,
|
durationMin = DEFAULT_DURATION_MIN,
|
||||||
durationMax = DEFAULT_DURATION_MAX,
|
durationMax = DEFAULT_DURATION_MAX,
|
||||||
initialDelayMax = DEFAULT_INITIAL_DELAY_MAX,
|
|
||||||
isInitial = false,
|
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const durationRange = durationMax - durationMin
|
const durationRange = durationMax - durationMin
|
||||||
|
|
@ -21,7 +18,7 @@ function createSnowflake(id, options = {}) {
|
||||||
id: id || crypto.randomUUID(),
|
id: id || crypto.randomUUID(),
|
||||||
left: Math.random() * 100,
|
left: Math.random() * 100,
|
||||||
duration: Math.random() * durationRange + durationMin,
|
duration: Math.random() * durationRange + durationMin,
|
||||||
delay: isInitial ? Math.random() * initialDelayMax : 0,
|
delay: 0, // Частицы появляются сразу
|
||||||
size: Math.random() * 10 + 10, // 10-20px
|
size: Math.random() * 10 + 10, // 10-20px
|
||||||
opacity: Math.random() * 0.5 + 0.5, // 0.5-1
|
opacity: Math.random() * 0.5 + 0.5, // 0.5-1
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
|
@ -31,6 +28,10 @@ function createSnowflake(id, options = {}) {
|
||||||
const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
const { currentThemeData } = useTheme()
|
const { currentThemeData } = useTheme()
|
||||||
const [snowflakes, setSnowflakes] = useState([])
|
const [snowflakes, setSnowflakes] = useState([])
|
||||||
|
const [windowSize, setWindowSize] = useState({
|
||||||
|
width: typeof window !== 'undefined' ? window.innerWidth : 1920,
|
||||||
|
height: typeof window !== 'undefined' ? window.innerHeight : 1080,
|
||||||
|
})
|
||||||
|
|
||||||
// Determine if particles should be enabled
|
// Determine if particles should be enabled
|
||||||
// Priority: room override (if explicitly set to true/false) > theme setting > default (true)
|
// Priority: room override (if explicitly set to true/false) > theme setting > default (true)
|
||||||
|
|
@ -52,9 +53,29 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
return currentThemeData?.settings?.particleSymbol || '❄'
|
return currentThemeData?.settings?.particleSymbol || '❄'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get particle density from theme settings
|
||||||
|
const getParticleDensity = () => {
|
||||||
|
return currentThemeData?.settings?.particleDensity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate target count based on density and screen size, or use fixed target count
|
||||||
|
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
|
// Get particle animation settings from theme with defaults
|
||||||
const getParticleTargetCount = () => {
|
const getParticleTargetCount = () => {
|
||||||
return currentThemeData?.settings?.particleTargetCount ?? DEFAULT_TARGET_COUNT
|
return calculateTargetCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParticleUpdateInterval = () => {
|
const getParticleUpdateInterval = () => {
|
||||||
|
|
@ -68,19 +89,27 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParticleInitialDelayMax = () => {
|
// Handle window resize to recalculate particle count
|
||||||
return currentThemeData?.settings?.particleInitialDelayMax ?? DEFAULT_INITIAL_DELAY_MAX
|
useEffect(() => {
|
||||||
}
|
const handleResize = () => {
|
||||||
|
setWindowSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const particlesEnabled = getParticlesEnabled()
|
const particlesEnabled = getParticlesEnabled()
|
||||||
const particleSymbol = getParticleSymbol()
|
const particleSymbol = getParticleSymbol()
|
||||||
const targetCount = getParticleTargetCount()
|
const targetCount = getParticleTargetCount()
|
||||||
const updateInterval = getParticleUpdateInterval()
|
const updateInterval = getParticleUpdateInterval()
|
||||||
const durationRange = getParticleDurationRange()
|
const durationRange = getParticleDurationRange()
|
||||||
const initialDelayMax = getParticleInitialDelayMax()
|
|
||||||
|
|
||||||
// Initialize snowflakes only if particles are enabled
|
// Initialize snowflakes only if particles are enabled
|
||||||
// Also re-initialize when theme changes (particle settings might change)
|
// Also re-initialize when theme changes or window size changes (particle settings might change)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!particlesEnabled) {
|
if (!particlesEnabled) {
|
||||||
setSnowflakes([])
|
setSnowflakes([])
|
||||||
|
|
@ -90,12 +119,10 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
createSnowflake(i, {
|
createSnowflake(i, {
|
||||||
durationMin: durationRange.min,
|
durationMin: durationRange.min,
|
||||||
durationMax: durationRange.max,
|
durationMax: durationRange.max,
|
||||||
initialDelayMax,
|
|
||||||
isInitial: true,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
setSnowflakes(initial)
|
setSnowflakes(initial)
|
||||||
}, [particlesEnabled, particleSymbol, targetCount, durationRange.min, durationRange.max, initialDelayMax, currentThemeData])
|
}, [particlesEnabled, particleSymbol, targetCount, durationRange.min, durationRange.max])
|
||||||
|
|
||||||
// Update cycle - remove old snowflakes and add new ones
|
// Update cycle - remove old snowflakes and add new ones
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -121,8 +148,6 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
createSnowflake(null, {
|
createSnowflake(null, {
|
||||||
durationMin: durationRange.min,
|
durationMin: durationRange.min,
|
||||||
durationMax: durationRange.max,
|
durationMax: durationRange.max,
|
||||||
initialDelayMax,
|
|
||||||
isInitial: false,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +157,7 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||||
}, updateInterval)
|
}, updateInterval)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [particlesEnabled, targetCount, updateInterval, durationRange.min, durationRange.max, initialDelayMax])
|
}, [particlesEnabled, targetCount, updateInterval, durationRange.min, durationRange.max])
|
||||||
|
|
||||||
// Don't render if particles are disabled
|
// Don't render if particles are disabled
|
||||||
if (!particlesEnabled) {
|
if (!particlesEnabled) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { authApi } from '../services/api';
|
import { authApi } from '../services/api';
|
||||||
|
import { getCookie, setCookie, deleteCookie } from '../utils/cookies';
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
|
|
@ -17,12 +18,37 @@ export const AuthProvider = ({ children }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('user');
|
// Миграция с localStorage на куки (обратная совместимость)
|
||||||
const storedToken = localStorage.getItem('token');
|
const storedUserLS = localStorage.getItem('user');
|
||||||
|
const storedTokenLS = localStorage.getItem('token');
|
||||||
|
|
||||||
|
if (storedUserLS && storedTokenLS) {
|
||||||
|
// Мигрируем в куки
|
||||||
|
const userData = JSON.parse(storedUserLS);
|
||||||
|
setCookie('player-id', userData.id);
|
||||||
|
setCookie('player-name', userData.name);
|
||||||
|
setCookie('player-token', storedTokenLS);
|
||||||
|
|
||||||
|
setUser(userData);
|
||||||
|
setToken(storedTokenLS);
|
||||||
|
|
||||||
|
// Очищаем localStorage (но оставляем app-theme и room-password-*)
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
} else {
|
||||||
|
// Загружаем из куков
|
||||||
|
const storedUserId = getCookie('player-id');
|
||||||
|
const storedUserName = getCookie('player-name');
|
||||||
|
const storedToken = getCookie('player-token');
|
||||||
|
|
||||||
if (storedUser && storedToken) {
|
if (storedUserId && storedUserName && storedToken) {
|
||||||
setUser(JSON.parse(storedUser));
|
const userData = {
|
||||||
setToken(storedToken);
|
id: storedUserId,
|
||||||
|
name: storedUserName,
|
||||||
|
};
|
||||||
|
setUser(userData);
|
||||||
|
setToken(storedToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -36,8 +62,9 @@ export const AuthProvider = ({ children }) => {
|
||||||
setUser(newUser);
|
setUser(newUser);
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(newUser));
|
setCookie('player-id', newUser.id);
|
||||||
localStorage.setItem('token', newToken);
|
setCookie('player-name', newUser.name);
|
||||||
|
setCookie('player-token', newToken);
|
||||||
|
|
||||||
return { user: newUser, token: newToken };
|
return { user: newUser, token: newToken };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -54,8 +81,9 @@ export const AuthProvider = ({ children }) => {
|
||||||
setUser(newUser);
|
setUser(newUser);
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(newUser));
|
setCookie('player-id', newUser.id);
|
||||||
localStorage.setItem('token', newToken);
|
setCookie('player-name', newUser.name);
|
||||||
|
setCookie('player-token', newToken);
|
||||||
|
|
||||||
return { user: newUser, token: newToken };
|
return { user: newUser, token: newToken };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -72,8 +100,9 @@ export const AuthProvider = ({ children }) => {
|
||||||
setUser(newUser);
|
setUser(newUser);
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(newUser));
|
setCookie('player-id', newUser.id);
|
||||||
localStorage.setItem('token', newToken);
|
setCookie('player-name', newUser.name);
|
||||||
|
setCookie('player-token', newToken);
|
||||||
|
|
||||||
return { user: newUser, token: newToken };
|
return { user: newUser, token: newToken };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -85,8 +114,29 @@ export const AuthProvider = ({ children }) => {
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
localStorage.removeItem('user');
|
deleteCookie('player-id');
|
||||||
localStorage.removeItem('token');
|
deleteCookie('player-name');
|
||||||
|
deleteCookie('player-token');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserName = async (name) => {
|
||||||
|
if (!token || !user) {
|
||||||
|
throw new Error('Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authApi.updateName(token, name);
|
||||||
|
const { user: updatedUser } = response.data;
|
||||||
|
|
||||||
|
setUser(updatedUser);
|
||||||
|
setCookie('player-id', updatedUser.id);
|
||||||
|
setCookie('player-name', updatedUser.name);
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update name error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
|
|
@ -97,6 +147,7 @@ export const AuthProvider = ({ children }) => {
|
||||||
register,
|
register,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
updateUserName,
|
||||||
isAuthenticated: !!user,
|
isAuthenticated: !!user,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,27 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||||
const fetchRoom = async () => {
|
const fetchRoom = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await roomsApi.getByCode(roomCode, password, user?.id);
|
|
||||||
|
// Проверяем, есть ли сохраненный пароль для этой комнаты
|
||||||
|
let roomPassword = password;
|
||||||
|
if (!roomPassword) {
|
||||||
|
const savedPassword = localStorage.getItem(`room-password-${roomCode}`);
|
||||||
|
if (savedPassword) {
|
||||||
|
roomPassword = savedPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await roomsApi.getByCode(roomCode, roomPassword, user?.id);
|
||||||
setRoom(response.data);
|
setRoom(response.data);
|
||||||
setParticipants(response.data.participants || []);
|
setParticipants(response.data.participants || []);
|
||||||
setError(null);
|
setError(null);
|
||||||
setRequiresPassword(false);
|
setRequiresPassword(false);
|
||||||
|
|
||||||
|
// Сохраняем пароль, если вход успешен
|
||||||
|
if (roomPassword) {
|
||||||
|
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Подключаться к WebSocket только после успешной загрузки комнаты
|
// ✅ Подключаться к WebSocket только после успешной загрузки комнаты
|
||||||
socketService.connect();
|
socketService.connect();
|
||||||
socketService.joinRoom(roomCode, user?.id);
|
socketService.joinRoom(roomCode, user?.id);
|
||||||
|
|
@ -34,6 +49,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||||
if (err.response?.status === 401) {
|
if (err.response?.status === 401) {
|
||||||
setRequiresPassword(true);
|
setRequiresPassword(true);
|
||||||
setError('Room password required');
|
setError('Room password required');
|
||||||
|
// Удаляем неверный сохраненный пароль
|
||||||
|
localStorage.removeItem(`room-password-${roomCode}`);
|
||||||
// ❌ НЕ подключаться к WebSocket, если требуется пароль
|
// ❌ НЕ подключаться к WebSocket, если требуется пароль
|
||||||
} else {
|
} else {
|
||||||
setError(err.response?.data?.message || err.message);
|
setError(err.response?.data?.message || err.message);
|
||||||
|
|
@ -186,6 +203,9 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setRequiresPassword(false);
|
setRequiresPassword(false);
|
||||||
|
|
||||||
|
// Сохраняем пароль в localStorage при успешном входе
|
||||||
|
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||||
|
|
||||||
// ✅ После успешной загрузки комнаты с паролем подключаемся к WebSocket
|
// ✅ После успешной загрузки комнаты с паролем подключаемся к WebSocket
|
||||||
socketService.connect();
|
socketService.connect();
|
||||||
socketService.joinRoom(roomCode, user?.id);
|
socketService.joinRoom(roomCode, user?.id);
|
||||||
|
|
@ -195,6 +215,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
||||||
if (err.response?.status === 401) {
|
if (err.response?.status === 401) {
|
||||||
setRequiresPassword(true);
|
setRequiresPassword(true);
|
||||||
setError('Incorrect password');
|
setError('Incorrect password');
|
||||||
|
// Удаляем неверный пароль
|
||||||
|
localStorage.removeItem(`room-password-${roomCode}`);
|
||||||
} else {
|
} else {
|
||||||
setError(err.response?.data?.message || err.message);
|
setError(err.response?.data?.message || err.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,48 @@ body {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name-editable {
|
||||||
|
color: gold;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-editable:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
text-shadow: 0 0 8px rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-inline {
|
||||||
|
background: rgba(255, 215, 0, 0.15);
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.5);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
color: gold;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 300px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-inline:focus {
|
||||||
|
border-color: gold;
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-inline:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-buttons {
|
.menu-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import ThemeSwitcher from '../components/ThemeSwitcher';
|
import ThemeSwitcher from '../components/ThemeSwitcher';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, loginAnonymous, isAuthenticated } = useAuth();
|
const { user, loginAnonymous, isAuthenticated, updateUserName } = useAuth();
|
||||||
|
const [editingName, setEditingName] = useState(false);
|
||||||
|
const [nameValue, setNameValue] = useState('');
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initAuth = async () => {
|
const initAuth = async () => {
|
||||||
|
|
@ -21,6 +24,44 @@ const Home = () => {
|
||||||
initAuth();
|
initAuth();
|
||||||
}, [isAuthenticated, loginAnonymous]);
|
}, [isAuthenticated, loginAnonymous]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setNameValue(user.name || '');
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleNameClick = () => {
|
||||||
|
setEditingName(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (e) => {
|
||||||
|
setNameValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameBlur = async () => {
|
||||||
|
setEditingName(false);
|
||||||
|
|
||||||
|
if (nameValue.trim() && nameValue.trim() !== user?.name) {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
await updateUserName(nameValue.trim());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update name:', error);
|
||||||
|
setNameValue(user?.name || '');
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setNameValue(user?.name || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateRoom = () => {
|
const handleCreateRoom = () => {
|
||||||
navigate('/create-room');
|
navigate('/create-room');
|
||||||
};
|
};
|
||||||
|
|
@ -36,9 +77,37 @@ const Home = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="home-container">
|
<div className="home-container">
|
||||||
<h1>100 к 1</h1>
|
<h1>100 к 1</h1>
|
||||||
<p className="welcome-text">
|
<div className="welcome-text">
|
||||||
{user ? `Привет, ${user.name}!` : 'Добро пожаловать!'}
|
{user ? (
|
||||||
</p>
|
<>
|
||||||
|
Привет,{' '}
|
||||||
|
{editingName ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={nameValue}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
onKeyPress={handleNameKeyPress}
|
||||||
|
className="name-input-inline"
|
||||||
|
autoFocus
|
||||||
|
maxLength={50}
|
||||||
|
disabled={isUpdating}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="name-editable"
|
||||||
|
onClick={handleNameClick}
|
||||||
|
title="Нажмите, чтобы изменить имя"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Добро пожаловать!'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="menu-buttons">
|
<div className="menu-buttons">
|
||||||
<button onClick={handleCreateRoom} className="menu-button primary">
|
<button onClick={handleCreateRoom} className="menu-button primary">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ export const authApi = {
|
||||||
createAnonymous: (name) => api.post('/auth/anonymous', { name }),
|
createAnonymous: (name) => api.post('/auth/anonymous', { name }),
|
||||||
register: (email, password, name) => api.post('/auth/register', { email, password, name }),
|
register: (email, password, name) => api.post('/auth/register', { email, password, name }),
|
||||||
login: (email, password) => api.post('/auth/login', { email, password }),
|
login: (email, password) => api.post('/auth/login', { email, password }),
|
||||||
|
updateName: (token, name) =>
|
||||||
|
api.patch('/auth/user', { name }, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rooms endpoints
|
// Rooms endpoints
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue