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
|
||||
particleDurationMax?: number
|
||||
particleInitialDelayMax?: number
|
||||
particleDensity?: number
|
||||
// Finish Screen Settings
|
||||
finishScreenTitle?: string
|
||||
finishScreenSubtitle?: string
|
||||
|
|
@ -317,6 +318,7 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = {
|
|||
particleDurationMin: 7,
|
||||
particleDurationMax: 10,
|
||||
particleInitialDelayMax: 10,
|
||||
particleDensity: 100,
|
||||
finishScreenTitle: 'Игра завершена!',
|
||||
finishScreenSubtitle: '',
|
||||
finishScreenBgColor: 'rgba(0, 0, 0, 0.5)',
|
||||
|
|
|
|||
|
|
@ -615,7 +615,29 @@ export function ThemeEditorDialog({
|
|||
}}
|
||||
/>
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -112,6 +112,11 @@ export class ThemeSettingsDto {
|
|||
@Type(() => Number)
|
||||
particleInitialDelayMax?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
particleDensity?: number;
|
||||
|
||||
// Finish Screen Settings
|
||||
@IsString()
|
||||
@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';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
@Post('anonymous')
|
||||
async createAnonymous(@Body('name') name?: string) {
|
||||
|
|
@ -19,4 +23,34 @@ export class AuthController {
|
|||
async login(@Body() dto: { email: string; password: string }) {
|
||||
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) {
|
||||
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 всем клиентам в комнате
|
||||
if (updatedRoom) {
|
||||
// Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате
|
||||
// WebSocket joinRoom может выполняться параллельно с REST API joinRoom
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
||||
// Также отправляем gameStateUpdated через broadcastFullState
|
||||
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
||||
|
|
@ -243,6 +247,9 @@ export class RoomsService {
|
|||
|
||||
// Отправляем событие roomUpdate всем клиентам в комнате
|
||||
if (updatedRoom) {
|
||||
// Небольшая задержка, чтобы дать время новому игроку присоединиться к WebSocket комнате
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
this.roomEventsService.emitRoomUpdate(updatedRoom.code, updatedRoom);
|
||||
// Также отправляем gameStateUpdated через broadcastFullState
|
||||
await this.gameGateway.broadcastFullState(updatedRoom.code);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,11 @@ const DEFAULT_TARGET_COUNT = 200
|
|||
const DEFAULT_UPDATE_INTERVAL = 1000
|
||||
const DEFAULT_DURATION_MIN = 7
|
||||
const DEFAULT_DURATION_MAX = 10
|
||||
const DEFAULT_INITIAL_DELAY_MAX = 10
|
||||
|
||||
function createSnowflake(id, options = {}) {
|
||||
const {
|
||||
durationMin = DEFAULT_DURATION_MIN,
|
||||
durationMax = DEFAULT_DURATION_MAX,
|
||||
initialDelayMax = DEFAULT_INITIAL_DELAY_MAX,
|
||||
isInitial = false,
|
||||
} = options
|
||||
|
||||
const durationRange = durationMax - durationMin
|
||||
|
|
@ -21,7 +18,7 @@ function createSnowflake(id, options = {}) {
|
|||
id: id || crypto.randomUUID(),
|
||||
left: Math.random() * 100,
|
||||
duration: Math.random() * durationRange + durationMin,
|
||||
delay: isInitial ? Math.random() * initialDelayMax : 0,
|
||||
delay: 0, // Частицы появляются сразу
|
||||
size: Math.random() * 10 + 10, // 10-20px
|
||||
opacity: Math.random() * 0.5 + 0.5, // 0.5-1
|
||||
createdAt: Date.now(),
|
||||
|
|
@ -31,6 +28,10 @@ function createSnowflake(id, options = {}) {
|
|||
const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
||||
const { currentThemeData } = useTheme()
|
||||
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
|
||||
// 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 || '❄'
|
||||
}
|
||||
|
||||
// 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
|
||||
const getParticleTargetCount = () => {
|
||||
return currentThemeData?.settings?.particleTargetCount ?? DEFAULT_TARGET_COUNT
|
||||
return calculateTargetCount()
|
||||
}
|
||||
|
||||
const getParticleUpdateInterval = () => {
|
||||
|
|
@ -68,19 +89,27 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getParticleInitialDelayMax = () => {
|
||||
return currentThemeData?.settings?.particleInitialDelayMax ?? DEFAULT_INITIAL_DELAY_MAX
|
||||
}
|
||||
// Handle window resize to recalculate particle count
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const particlesEnabled = getParticlesEnabled()
|
||||
const particleSymbol = getParticleSymbol()
|
||||
const targetCount = getParticleTargetCount()
|
||||
const updateInterval = getParticleUpdateInterval()
|
||||
const durationRange = getParticleDurationRange()
|
||||
const initialDelayMax = getParticleInitialDelayMax()
|
||||
|
||||
// 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(() => {
|
||||
if (!particlesEnabled) {
|
||||
setSnowflakes([])
|
||||
|
|
@ -90,12 +119,10 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
createSnowflake(i, {
|
||||
durationMin: durationRange.min,
|
||||
durationMax: durationRange.max,
|
||||
initialDelayMax,
|
||||
isInitial: true,
|
||||
})
|
||||
)
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -121,8 +148,6 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
createSnowflake(null, {
|
||||
durationMin: durationRange.min,
|
||||
durationMax: durationRange.max,
|
||||
initialDelayMax,
|
||||
isInitial: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
@ -132,7 +157,7 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => {
|
|||
}, updateInterval)
|
||||
|
||||
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
|
||||
if (!particlesEnabled) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { authApi } from '../services/api';
|
||||
import { getCookie, setCookie, deleteCookie } from '../utils/cookies';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
|
|
@ -17,12 +18,37 @@ export const AuthProvider = ({ children }) => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const storedToken = localStorage.getItem('token');
|
||||
// Миграция с localStorage на куки (обратная совместимость)
|
||||
const storedUserLS = localStorage.getItem('user');
|
||||
const storedTokenLS = localStorage.getItem('token');
|
||||
|
||||
if (storedUser && storedToken) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
setToken(storedToken);
|
||||
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 (storedUserId && storedUserName && storedToken) {
|
||||
const userData = {
|
||||
id: storedUserId,
|
||||
name: storedUserName,
|
||||
};
|
||||
setUser(userData);
|
||||
setToken(storedToken);
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
|
@ -36,8 +62,9 @@ export const AuthProvider = ({ children }) => {
|
|||
setUser(newUser);
|
||||
setToken(newToken);
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(newUser));
|
||||
localStorage.setItem('token', newToken);
|
||||
setCookie('player-id', newUser.id);
|
||||
setCookie('player-name', newUser.name);
|
||||
setCookie('player-token', newToken);
|
||||
|
||||
return { user: newUser, token: newToken };
|
||||
} catch (error) {
|
||||
|
|
@ -54,8 +81,9 @@ export const AuthProvider = ({ children }) => {
|
|||
setUser(newUser);
|
||||
setToken(newToken);
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(newUser));
|
||||
localStorage.setItem('token', newToken);
|
||||
setCookie('player-id', newUser.id);
|
||||
setCookie('player-name', newUser.name);
|
||||
setCookie('player-token', newToken);
|
||||
|
||||
return { user: newUser, token: newToken };
|
||||
} catch (error) {
|
||||
|
|
@ -72,8 +100,9 @@ export const AuthProvider = ({ children }) => {
|
|||
setUser(newUser);
|
||||
setToken(newToken);
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(newUser));
|
||||
localStorage.setItem('token', newToken);
|
||||
setCookie('player-id', newUser.id);
|
||||
setCookie('player-name', newUser.name);
|
||||
setCookie('player-token', newToken);
|
||||
|
||||
return { user: newUser, token: newToken };
|
||||
} catch (error) {
|
||||
|
|
@ -85,8 +114,29 @@ export const AuthProvider = ({ children }) => {
|
|||
const logout = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
deleteCookie('player-id');
|
||||
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 = {
|
||||
|
|
@ -97,6 +147,7 @@ export const AuthProvider = ({ children }) => {
|
|||
register,
|
||||
login,
|
||||
logout,
|
||||
updateUserName,
|
||||
isAuthenticated: !!user,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,27 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
const fetchRoom = async () => {
|
||||
try {
|
||||
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);
|
||||
setParticipants(response.data.participants || []);
|
||||
setError(null);
|
||||
setRequiresPassword(false);
|
||||
|
||||
// Сохраняем пароль, если вход успешен
|
||||
if (roomPassword) {
|
||||
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||
}
|
||||
|
||||
// ✅ Подключаться к WebSocket только после успешной загрузки комнаты
|
||||
socketService.connect();
|
||||
socketService.joinRoom(roomCode, user?.id);
|
||||
|
|
@ -34,6 +49,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
if (err.response?.status === 401) {
|
||||
setRequiresPassword(true);
|
||||
setError('Room password required');
|
||||
// Удаляем неверный сохраненный пароль
|
||||
localStorage.removeItem(`room-password-${roomCode}`);
|
||||
// ❌ НЕ подключаться к WebSocket, если требуется пароль
|
||||
} else {
|
||||
setError(err.response?.data?.message || err.message);
|
||||
|
|
@ -186,6 +203,9 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
setError(null);
|
||||
setRequiresPassword(false);
|
||||
|
||||
// Сохраняем пароль в localStorage при успешном входе
|
||||
localStorage.setItem(`room-password-${roomCode}`, roomPassword);
|
||||
|
||||
// ✅ После успешной загрузки комнаты с паролем подключаемся к WebSocket
|
||||
socketService.connect();
|
||||
socketService.joinRoom(roomCode, user?.id);
|
||||
|
|
@ -195,6 +215,8 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
if (err.response?.status === 401) {
|
||||
setRequiresPassword(true);
|
||||
setError('Incorrect password');
|
||||
// Удаляем неверный пароль
|
||||
localStorage.removeItem(`room-password-${roomCode}`);
|
||||
} else {
|
||||
setError(err.response?.data?.message || err.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,6 +186,48 @@ body {
|
|||
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 {
|
||||
display: flex;
|
||||
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 { useAuth } from '../context/AuthContext';
|
||||
import ThemeSwitcher from '../components/ThemeSwitcher';
|
||||
|
||||
const Home = () => {
|
||||
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(() => {
|
||||
const initAuth = async () => {
|
||||
|
|
@ -21,6 +24,44 @@ const Home = () => {
|
|||
initAuth();
|
||||
}, [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 = () => {
|
||||
navigate('/create-room');
|
||||
};
|
||||
|
|
@ -36,9 +77,37 @@ const Home = () => {
|
|||
</div>
|
||||
<div className="home-container">
|
||||
<h1>100 к 1</h1>
|
||||
<p className="welcome-text">
|
||||
{user ? `Привет, ${user.name}!` : 'Добро пожаловать!'}
|
||||
</p>
|
||||
<div className="welcome-text">
|
||||
{user ? (
|
||||
<>
|
||||
Привет,{' '}
|
||||
{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">
|
||||
<button onClick={handleCreateRoom} className="menu-button primary">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export const authApi = {
|
|||
createAnonymous: (name) => api.post('/auth/anonymous', { name }),
|
||||
register: (email, password, name) => api.post('/auth/register', { email, password, name }),
|
||||
login: (email, password) => api.post('/auth/login', { email, password }),
|
||||
updateName: (token, name) =>
|
||||
api.patch('/auth/user', { name }, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}),
|
||||
};
|
||||
|
||||
// Rooms endpoints
|
||||
|
|
|
|||
Loading…
Reference in a new issue