This commit is contained in:
Dmitry 2026-01-11 00:14:59 +03:00
parent 2c04a3c757
commit 58caf2470e
7 changed files with 203 additions and 35 deletions

View file

@ -27,19 +27,21 @@ export class AdminFeatureFlagsService {
async update(key: string, dto: UpdateFeatureFlagDto) { async update(key: string, dto: UpdateFeatureFlagDto) {
try { try {
const flag = await this.prisma.featureFlag.update({ const flag = await this.prisma.featureFlag.upsert({
where: { key }, where: { key },
data: { update: {
enabled: dto.enabled, enabled: dto.enabled,
description: dto.description, description: dto.description,
}, },
create: {
key,
enabled: dto.enabled,
description: dto.description || null,
},
}); });
return flag; return flag;
} catch (error) { } catch (error) {
if (error.code === 'P2025') {
throw new NotFoundException(`Feature flag with key "${key}" not found`);
}
throw new BadRequestException(`Failed to update feature flag: ${error.message}`); throw new BadRequestException(`Failed to update feature flag: ${error.message}`);
} }
} }

View file

@ -38,6 +38,16 @@ export class AdminThemesController {
return this.adminThemesService.create(createThemeDto, req.user.sub); return this.adminThemesService.create(createThemeDto, req.user.sub);
} }
@Patch('reorder')
reorder(@Body() reorderDto: ReorderThemesDto) {
return this.adminThemesService.reorderThemes(reorderDto.themeIds);
}
@Patch(':id/set-default')
setDefault(@Param('id') id: string) {
return this.adminThemesService.setDefaultTheme(id);
}
@Patch(':id') @Patch(':id')
update(@Param('id') id: string, @Body() updateThemeDto: UpdateThemeDto) { update(@Param('id') id: string, @Body() updateThemeDto: UpdateThemeDto) {
return this.adminThemesService.update(id, updateThemeDto); return this.adminThemesService.update(id, updateThemeDto);
@ -47,14 +57,4 @@ export class AdminThemesController {
remove(@Param('id') id: string) { remove(@Param('id') id: string) {
return this.adminThemesService.remove(id); return this.adminThemesService.remove(id);
} }
@Patch(':id/set-default')
setDefault(@Param('id') id: string) {
return this.adminThemesService.setDefaultTheme(id);
}
@Patch('reorder')
reorder(@Body() reorderDto: ReorderThemesDto) {
return this.adminThemesService.reorderThemes(reorderDto.themeIds);
}
} }

View file

@ -103,6 +103,117 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
@SubscribeMessage('joinRoom') @SubscribeMessage('joinRoom')
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) { async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
client.join(payload.roomCode); client.join(payload.roomCode);
// Получаем полное состояние для отправки присоединившемуся клиенту
const room = (await this.prisma.room.findUnique({
where: { code: payload.roomCode },
include: {
participants: {
where: { isActive: true },
orderBy: { joinedAt: 'asc' }
},
roomPack: true,
host: { select: { id: true, name: true } },
theme: true
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
// Используем тот же метод, что и в broadcastFullState, но отправляем напрямую клиенту
const roomPackQuestions = (room.roomPack as unknown as { questions?: any } | null)?.questions;
let questions: Question[] = [];
if (roomPackQuestions) {
if (Array.isArray(roomPackQuestions)) {
questions = roomPackQuestions as Question[];
} else if (typeof roomPackQuestions === 'string') {
try {
questions = JSON.parse(roomPackQuestions) as Question[];
} catch (e) {
console.error('Error parsing roomPack.questions:', e);
questions = [];
}
}
}
let currentQuestionId = (room.currentQuestionId as string | null) || null;
if (currentQuestionId) {
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
if (!questionExists) {
currentQuestionId = null;
}
}
if (!currentQuestionId && questions.length > 0) {
const firstQuestion = questions[0];
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
currentQuestionId = firstQuestion.id;
await this.prisma.room.update({
where: { id: room.id },
data: {
currentQuestionId: currentQuestionId,
currentQuestionIndex: 0
}
});
}
}
let currentPlayerId = room.currentPlayerId;
if (!currentPlayerId && room.participants.length > 0) {
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
if (firstParticipant) {
currentPlayerId = firstParticipant.id;
await this.prisma.room.update({
where: { id: room.id },
data: { currentPlayerId: currentPlayerId }
});
}
}
const fullState = {
roomId: room.id,
roomCode: room.code,
status: room.status,
currentQuestionId: currentQuestionId,
currentPlayerId: currentPlayerId,
revealedAnswers: room.revealedAnswers as RevealedAnswers,
isGameOver: room.isGameOver,
hostId: room.hostId,
themeId: (room as any).themeId || null,
voiceMode: (room as any).voiceMode !== undefined ? (room as any).voiceMode : false,
particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null,
maxPlayers: (room as any).maxPlayers || 10,
participants: room.participants.map((p) => ({
id: p.id,
userId: p.userId,
name: p.name,
role: p.role,
score: p.score
})),
questions: questions.map((q: any) => {
const questionId = q.id || (typeof q === 'object' && 'id' in q ? q.id : null);
if (!questionId) {
console.warn('⚠️ Question without ID:', q);
}
return {
id: questionId || `temp-${Math.random()}`,
text: q.text || '',
answers: (q.answers || []).map((a: any) => ({
id: a.id || `answer-${Math.random()}`,
text: a.text || '',
points: a.points || 0
}))
};
})
};
// Отправляем состояние напрямую присоединившемуся клиенту
client.emit('gameStateUpdated', fullState);
}
// Также отправляем всем остальным в комнате (broadcast)
await this.broadcastFullState(payload.roomCode); await this.broadcastFullState(payload.roomCode);
} }
@ -427,6 +538,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
isGameOver: room.isGameOver, isGameOver: room.isGameOver,
hostId: room.hostId, hostId: room.hostId,
themeId: (room as any).themeId || null, themeId: (room as any).themeId || null,
voiceMode: (room as any).voiceMode !== undefined ? (room as any).voiceMode : false,
particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null, particlesEnabled: (room as any).particlesEnabled !== undefined ? (room as any).particlesEnabled : null,
maxPlayers: (room as any).maxPlayers || 10, maxPlayers: (room as any).maxPlayers || 10,
participants: room.participants.map((p) => ({ participants: room.participants.map((p) => ({

View file

@ -132,6 +132,10 @@
row-gap: clamp(6px, 0.8vh, 12px); row-gap: clamp(6px, 0.8vh, 12px);
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto;
/* Scrollbar styling */
scrollbar-width: thin;
scrollbar-color: rgba(255, 215, 0, 0.5) rgba(255, 255, 255, 0.1);
} }
@media (min-width: 900px) { @media (min-width: 900px) {
@ -149,6 +153,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.answers-grid { .answers-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-auto-rows: minmax(auto, clamp(100px, 15vh, 140px));
} }
} }
@ -162,7 +167,27 @@
/* Для телефонов в портретной ориентации */ /* Для телефонов в портретной ориентации */
@media (max-width: 768px) and (max-height: 900px) { @media (max-width: 768px) and (max-height: 900px) {
.answers-grid { .answers-grid {
grid-auto-rows: minmax(auto, 150px); grid-auto-rows: minmax(auto, 120px);
} }
} }
/* Webkit scrollbar styling for answers-grid */
.answers-grid::-webkit-scrollbar {
width: 8px;
}
.answers-grid::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.answers-grid::-webkit-scrollbar-thumb {
background: rgba(255, 215, 0, 0.5);
border-radius: 4px;
transition: background 0.3s ease;
}
.answers-grid::-webkit-scrollbar-thumb:hover {
background: rgba(255, 215, 0, 0.7);
}

View file

@ -69,14 +69,30 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
setParticipants(state.participants); setParticipants(state.participants);
} }
// Также обновляем статус комнаты // Обновляем базовую информацию о комнате из состояния
if (state.status) { setRoom(prevRoom => {
setRoom(prevRoom => prevRoom ? { ...prevRoom, status: state.status } : prevRoom); if (!prevRoom) return prevRoom;
// Если игра началась, вызываем callback const updatedRoom = { ...prevRoom };
if (state.status === 'PLAYING' && onGameStarted) {
onGameStarted(state); 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);
} }
}; };
@ -115,6 +131,16 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => { const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => {
try { try {
const response = await roomsApi.join(roomId, userId, name, role); const response = await roomsApi.join(roomId, userId, name, role);
// После успешного присоединения запрашиваем полное состояние через WebSocket
// Это гарантирует получение актуального состояния (список игроков, тема, voiceMode)
if (response.data?.code) {
// Небольшая задержка, чтобы убедиться, что WebSocket подключен
setTimeout(() => {
socketService.emit('requestFullState', { roomCode: response.data.code });
}, 100);
}
return response.data; return response.data;
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);

View file

@ -32,6 +32,7 @@ const GamePage = () => {
hostId: null, hostId: null,
roomCode: null, roomCode: null,
themeId: null, themeId: null,
voiceMode: false,
particlesEnabled: null, // null = использовать настройку из темы, true/false = override particlesEnabled: null, // null = использовать настройку из темы, true/false = override
maxPlayers: 10, maxPlayers: 10,
}); });

View file

@ -218,6 +218,7 @@ const RoomPage = () => {
const handleGameStateUpdated = (state) => { const handleGameStateUpdated = (state) => {
const currentThemeId = state.themeId || null; const currentThemeId = state.themeId || null;
// Применяем тему если она изменилась или если это первое присоединение (previousThemeIdRef.current === null)
if (currentThemeId !== previousThemeIdRef.current) { if (currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId; previousThemeIdRef.current = currentThemeId;
if (currentThemeId) { if (currentThemeId) {
@ -226,22 +227,23 @@ const RoomPage = () => {
} }
}; };
// Также проверяем тему из room при изменении
if (room?.themeId) {
const currentThemeId = room.themeId || null;
if (currentThemeId !== previousThemeIdRef.current) {
previousThemeIdRef.current = currentThemeId;
if (currentThemeId) {
changeTheme(currentThemeId);
}
}
}
socketService.on('gameStateUpdated', handleGameStateUpdated); socketService.on('gameStateUpdated', handleGameStateUpdated);
return () => { return () => {
socketService.off('gameStateUpdated', handleGameStateUpdated); socketService.off('gameStateUpdated', handleGameStateUpdated);
}; };
}, [roomCode, room, changeTheme]); }, [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]);
const handleStartGame = () => { const handleStartGame = () => {
startGame(); startGame();