stiff
This commit is contained in:
parent
2c04a3c757
commit
58caf2470e
7 changed files with 203 additions and 35 deletions
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue