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) {
|
||||
try {
|
||||
const flag = await this.prisma.featureFlag.update({
|
||||
const flag = await this.prisma.featureFlag.upsert({
|
||||
where: { key },
|
||||
data: {
|
||||
update: {
|
||||
enabled: dto.enabled,
|
||||
description: dto.description,
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
enabled: dto.enabled,
|
||||
description: dto.description || null,
|
||||
},
|
||||
});
|
||||
|
||||
return flag;
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ export class AdminThemesController {
|
|||
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')
|
||||
update(@Param('id') id: string, @Body() updateThemeDto: UpdateThemeDto) {
|
||||
return this.adminThemesService.update(id, updateThemeDto);
|
||||
|
|
@ -47,14 +57,4 @@ export class AdminThemesController {
|
|||
remove(@Param('id') id: string) {
|
||||
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')
|
||||
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -427,6 +538,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
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) => ({
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@
|
|||
row-gap: clamp(6px, 0.8vh, 12px);
|
||||
flex: 1;
|
||||
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) {
|
||||
|
|
@ -149,6 +153,7 @@
|
|||
@media (max-width: 768px) {
|
||||
.answers-grid {
|
||||
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) {
|
||||
.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,15 +69,31 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
|
|||
setParticipants(state.participants);
|
||||
}
|
||||
|
||||
// Также обновляем статус комнаты
|
||||
// Обновляем базовую информацию о комнате из состояния
|
||||
setRoom(prevRoom => {
|
||||
if (!prevRoom) return prevRoom;
|
||||
|
||||
const updatedRoom = { ...prevRoom };
|
||||
|
||||
if (state.status) {
|
||||
setRoom(prevRoom => prevRoom ? { ...prevRoom, status: state.status } : prevRoom);
|
||||
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') => {
|
||||
try {
|
||||
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;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const GamePage = () => {
|
|||
hostId: null,
|
||||
roomCode: null,
|
||||
themeId: null,
|
||||
voiceMode: false,
|
||||
particlesEnabled: null, // null = использовать настройку из темы, true/false = override
|
||||
maxPlayers: 10,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ const RoomPage = () => {
|
|||
|
||||
const handleGameStateUpdated = (state) => {
|
||||
const currentThemeId = state.themeId || null;
|
||||
// Применяем тему если она изменилась или если это первое присоединение (previousThemeIdRef.current === null)
|
||||
if (currentThemeId !== previousThemeIdRef.current) {
|
||||
previousThemeIdRef.current = 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);
|
||||
return () => {
|
||||
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 = () => {
|
||||
startGame();
|
||||
|
|
|
|||
Loading…
Reference in a new issue