room
This commit is contained in:
parent
e7c38d102e
commit
fee1a5a36d
16 changed files with 651 additions and 119 deletions
|
|
@ -7,6 +7,7 @@ generator client {
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|
@ -58,13 +59,11 @@ model Room {
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
finishedAt DateTime?
|
finishedAt DateTime?
|
||||||
|
|
||||||
// Временный пак для комнаты (если хост редактирует вопросы)
|
|
||||||
customQuestions Json? // Кастомные вопросы для этой комнаты
|
|
||||||
|
|
||||||
// Связи
|
// Связи
|
||||||
host User @relation("HostedRooms", fields: [hostId], references: [id])
|
host User @relation("HostedRooms", fields: [hostId], references: [id])
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
questionPack QuestionPack? @relation(fields: [questionPackId], references: [id])
|
questionPack QuestionPack? @relation(fields: [questionPackId], references: [id])
|
||||||
|
roomPack RoomPack?
|
||||||
gameHistory GameHistory?
|
gameHistory GameHistory?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +117,26 @@ model QuestionPack {
|
||||||
|
|
||||||
creator User @relation(fields: [createdBy], references: [id])
|
creator User @relation(fields: [createdBy], references: [id])
|
||||||
rooms Room[]
|
rooms Room[]
|
||||||
|
roomPacks RoomPack[] @relation("RoomPackSource")
|
||||||
|
}
|
||||||
|
|
||||||
|
model RoomPack {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
roomId String @unique
|
||||||
|
name String
|
||||||
|
description String @default("")
|
||||||
|
sourcePackId String?
|
||||||
|
questions Json @default("[]")
|
||||||
|
questionCount Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
|
||||||
|
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||||
|
sourcePack QuestionPack? @relation("RoomPackSource", fields: [sourcePackId], references: [id])
|
||||||
|
|
||||||
|
@@index([roomId])
|
||||||
|
@@index([sourcePackId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model GameHistory {
|
model GameHistory {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { Server, Socket } from 'socket.io';
|
||||||
import { RoomsService } from '../rooms/rooms.service';
|
import { RoomsService } from '../rooms/rooms.service';
|
||||||
import { RoomEventsService } from './room-events.service';
|
import { RoomEventsService } from './room-events.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { RoomPackService } from '../room-pack/room-pack.service';
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: {
|
cors: {
|
||||||
|
|
@ -27,6 +28,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
private roomsService: RoomsService,
|
private roomsService: RoomsService,
|
||||||
private roomEventsService: RoomEventsService,
|
private roomEventsService: RoomEventsService,
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
|
private roomPackService: RoomPackService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
afterInit(server: Server) {
|
afterInit(server: Server) {
|
||||||
|
|
@ -181,19 +183,40 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
|
|
||||||
@SubscribeMessage('updateCustomQuestions')
|
@SubscribeMessage('updateCustomQuestions')
|
||||||
async handleUpdateCustomQuestions(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any }) {
|
async handleUpdateCustomQuestions(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any }) {
|
||||||
|
// DEPRECATED: Use updateRoomPack instead
|
||||||
|
return this.handleUpdateRoomPack(client, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('updateRoomPack')
|
||||||
|
async handleUpdateRoomPack(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any[] }) {
|
||||||
const isHost = await this.isHost(payload.roomId, payload.userId);
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
||||||
if (!isHost) {
|
if (!isHost) {
|
||||||
client.emit('error', { message: 'Only the host can update questions' });
|
client.emit('error', { message: 'Only the host can update questions' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.room.update({
|
const room = await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
||||||
where: { id: payload.roomId },
|
this.server.to(payload.roomCode).emit('roomPackUpdated', room);
|
||||||
data: { customQuestions: payload.questions },
|
}
|
||||||
});
|
|
||||||
|
@SubscribeMessage('importQuestions')
|
||||||
|
async handleImportQuestions(client: Socket, payload: {
|
||||||
|
roomId: string;
|
||||||
|
roomCode: string;
|
||||||
|
userId: string;
|
||||||
|
sourcePackId: string;
|
||||||
|
questionIndices: number[];
|
||||||
|
}) {
|
||||||
|
const isHost = await this.isHost(payload.roomId, payload.userId);
|
||||||
|
if (!isHost) {
|
||||||
|
client.emit('error', { message: 'Only the host can import questions' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices);
|
||||||
|
|
||||||
const room = await this.roomsService.getRoomByCode(payload.roomCode);
|
const room = await this.roomsService.getRoomByCode(payload.roomCode);
|
||||||
this.server.to(payload.roomCode).emit('customQuestionsUpdated', room);
|
this.server.to(payload.roomCode).emit('roomPackUpdated', room);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('kickPlayer')
|
@SubscribeMessage('kickPlayer')
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { GameGateway } from './game.gateway';
|
import { GameGateway } from './game.gateway';
|
||||||
import { RoomEventsService } from './room-events.service';
|
import { RoomEventsService } from './room-events.service';
|
||||||
import { RoomsModule } from '../rooms/rooms.module';
|
import { RoomsModule } from '../rooms/rooms.module';
|
||||||
|
import { RoomPackModule } from '../room-pack/room-pack.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [forwardRef(() => RoomsModule)],
|
imports: [forwardRef(() => RoomsModule), RoomPackModule],
|
||||||
providers: [GameGateway, RoomEventsService],
|
providers: [GameGateway, RoomEventsService],
|
||||||
exports: [RoomEventsService],
|
exports: [RoomEventsService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,12 @@ export class RoomEventsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitRoomPackUpdated(roomCode: string, data: any) {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.to(roomCode).emit('roomPackUpdated', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitPlayerKicked(roomCode: string, data: any) {
|
emitPlayerKicked(roomCode: string, data: any) {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.to(roomCode).emit('playerKicked', data);
|
this.server.to(roomCode).emit('playerKicked', data);
|
||||||
|
|
|
||||||
10
backend/src/room-pack/room-pack.module.ts
Normal file
10
backend/src/room-pack/room-pack.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RoomPackService } from './room-pack.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [RoomPackService],
|
||||||
|
exports: [RoomPackService],
|
||||||
|
})
|
||||||
|
export class RoomPackModule {}
|
||||||
119
backend/src/room-pack/room-pack.service.ts
Normal file
119
backend/src/room-pack/room-pack.service.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoomPackService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create room pack (called when room is created)
|
||||||
|
*/
|
||||||
|
async create(roomId: string, sourcePackId?: string) {
|
||||||
|
const name = `Room Pack ${Date.now()}`;
|
||||||
|
const description = sourcePackId
|
||||||
|
? 'Copied from source pack'
|
||||||
|
: 'Custom room questions';
|
||||||
|
|
||||||
|
let questions = [];
|
||||||
|
let questionCount = 0;
|
||||||
|
|
||||||
|
// If source pack provided, copy questions
|
||||||
|
if (sourcePackId) {
|
||||||
|
const sourcePack = await this.prisma.questionPack.findUnique({
|
||||||
|
where: { id: sourcePackId },
|
||||||
|
select: { questions: true, questionCount: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sourcePack) {
|
||||||
|
questions = sourcePack.questions;
|
||||||
|
questionCount = sourcePack.questionCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.roomPack.create({
|
||||||
|
data: {
|
||||||
|
roomId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
sourcePackId,
|
||||||
|
questions,
|
||||||
|
questionCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room pack by roomId
|
||||||
|
*/
|
||||||
|
async findByRoomId(roomId: string) {
|
||||||
|
return this.prisma.roomPack.findUnique({
|
||||||
|
where: { roomId },
|
||||||
|
include: { sourcePack: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update room pack questions
|
||||||
|
*/
|
||||||
|
async updateQuestions(roomId: string, questions: any[]) {
|
||||||
|
const questionCount = Array.isArray(questions) ? questions.length : 0;
|
||||||
|
|
||||||
|
return this.prisma.roomPack.update({
|
||||||
|
where: { roomId },
|
||||||
|
data: {
|
||||||
|
questions,
|
||||||
|
questionCount,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add questions from another pack (import/copy)
|
||||||
|
*/
|
||||||
|
async importQuestions(roomId: string, sourcePackId: string, questionIndices: number[]) {
|
||||||
|
const roomPack = await this.findByRoomId(roomId);
|
||||||
|
const sourcePack = await this.prisma.questionPack.findUnique({
|
||||||
|
where: { id: sourcePackId },
|
||||||
|
select: { questions: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sourcePack || !Array.isArray(sourcePack.questions)) {
|
||||||
|
throw new Error('Source pack not found or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing questions
|
||||||
|
const existingQuestions = Array.isArray(roomPack.questions)
|
||||||
|
? roomPack.questions
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Import selected questions (create copies)
|
||||||
|
const questionsToImport = questionIndices
|
||||||
|
.map(idx => sourcePack.questions[idx])
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(q => ({ ...q })); // Deep copy
|
||||||
|
|
||||||
|
const updatedQuestions = [...existingQuestions, ...questionsToImport];
|
||||||
|
|
||||||
|
return this.updateQuestions(roomId, updatedQuestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete room pack
|
||||||
|
*/
|
||||||
|
async softDelete(roomId: string) {
|
||||||
|
return this.prisma.roomPack.update({
|
||||||
|
where: { roomId },
|
||||||
|
data: { deletedAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hard delete room pack (for cleanup)
|
||||||
|
*/
|
||||||
|
async hardDelete(roomId: string) {
|
||||||
|
return this.prisma.roomPack.delete({
|
||||||
|
where: { roomId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,14 @@ export class RoomsController {
|
||||||
return this.roomsService.updateCustomQuestions(roomId, dto.questions);
|
return this.roomsService.updateCustomQuestions(roomId, dto.questions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':roomId/room-pack')
|
||||||
|
async updateRoomPack(
|
||||||
|
@Param('roomId') roomId: string,
|
||||||
|
@Body() dto: { questions: any[] }
|
||||||
|
) {
|
||||||
|
return this.roomsService.updateRoomPack(roomId, dto.questions);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':roomId/questions')
|
@Get(':roomId/questions')
|
||||||
async getEffectiveQuestions(@Param('roomId') roomId: string) {
|
async getEffectiveQuestions(@Param('roomId') roomId: string) {
|
||||||
return this.roomsService.getEffectiveQuestions(roomId);
|
return this.roomsService.getEffectiveQuestions(roomId);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { RoomsService } from './rooms.service';
|
import { RoomsService } from './rooms.service';
|
||||||
import { RoomsController } from './rooms.controller';
|
import { RoomsController } from './rooms.controller';
|
||||||
import { GameModule } from '../game/game.module';
|
import { GameModule } from '../game/game.module';
|
||||||
|
import { RoomPackModule } from '../room-pack/room-pack.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [forwardRef(() => GameModule)],
|
imports: [forwardRef(() => GameModule), RoomPackModule],
|
||||||
controllers: [RoomsController],
|
controllers: [RoomsController],
|
||||||
providers: [RoomsService],
|
providers: [RoomsService],
|
||||||
exports: [RoomsService],
|
exports: [RoomsService],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { RoomEventsService } from '../game/room-events.service';
|
import { RoomEventsService } from '../game/room-events.service';
|
||||||
|
import { RoomPackService } from '../room-pack/room-pack.service';
|
||||||
|
|
||||||
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
|
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ export class RoomsService {
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
@Inject(forwardRef(() => RoomEventsService))
|
@Inject(forwardRef(() => RoomEventsService))
|
||||||
private roomEventsService: RoomEventsService,
|
private roomEventsService: RoomEventsService,
|
||||||
|
private roomPackService: RoomPackService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createRoom(hostId: string, questionPackId?: string, settings?: any) {
|
async createRoom(hostId: string, questionPackId?: string, settings?: any) {
|
||||||
|
|
@ -46,7 +48,11 @@ export class RoomsService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return room;
|
// Create RoomPack (always, even if empty)
|
||||||
|
await this.roomPackService.create(room.id, questionPackId);
|
||||||
|
|
||||||
|
// Return room with roomPack
|
||||||
|
return this.getRoomByCode(room.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRoomByCode(code: string) {
|
async getRoomByCode(code: string) {
|
||||||
|
|
@ -58,6 +64,7 @@ export class RoomsService {
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
},
|
},
|
||||||
questionPack: true,
|
questionPack: true,
|
||||||
|
roomPack: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -125,42 +132,45 @@ export class RoomsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCustomQuestions(roomId: string, questions: any) {
|
async updateCustomQuestions(roomId: string, questions: any) {
|
||||||
const room = await this.prisma.room.update({
|
// DEPRECATED: Use updateRoomPack instead
|
||||||
|
return this.updateRoomPack(roomId, questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRoomPack(roomId: string, questions: any[]) {
|
||||||
|
await this.roomPackService.updateQuestions(roomId, questions);
|
||||||
|
|
||||||
|
const room = await this.prisma.room.findUnique({
|
||||||
where: { id: roomId },
|
where: { id: roomId },
|
||||||
data: {
|
|
||||||
customQuestions: questions,
|
|
||||||
currentQuestionIndex: 0,
|
|
||||||
revealedAnswers: {},
|
|
||||||
},
|
|
||||||
include: {
|
include: {
|
||||||
host: true,
|
host: true,
|
||||||
participants: {
|
participants: {
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
},
|
},
|
||||||
questionPack: true,
|
questionPack: true,
|
||||||
|
roomPack: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.roomEventsService.emitCustomQuestionsUpdated(room.code, room);
|
this.roomEventsService.emitRoomPackUpdated(room.code, room);
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEffectiveQuestions(roomId: string) {
|
async getEffectiveQuestions(roomId: string) {
|
||||||
const room = await this.prisma.room.findUnique({
|
const room = await this.prisma.room.findUnique({
|
||||||
where: { id: roomId },
|
where: { id: roomId },
|
||||||
include: { questionPack: true },
|
include: { roomPack: true, questionPack: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть кастомные вопросы, используем их
|
// Priority 1: RoomPack questions
|
||||||
if (room.customQuestions) {
|
if (room.roomPack && room.roomPack.questions) {
|
||||||
return room.customQuestions;
|
return room.roomPack.questions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе используем вопросы из пака
|
// Priority 2: QuestionPack (fallback for legacy rooms)
|
||||||
if (room.questionPack) {
|
if (room.questionPack) {
|
||||||
return room.questionPack.questions;
|
return room.questionPack.questions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -433,3 +433,168 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pack Import Styles */
|
||||||
|
.questions-modal-pack-import-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-modal-pack-import-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(250, 112, 154, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-section h3 {
|
||||||
|
color: #ffd700;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-select:focus {
|
||||||
|
border-color: #ffd700;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-select option {
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-questions-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-questions-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid rgba(255, 215, 0, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-confirm-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-confirm-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(78, 205, 196, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-confirm-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-questions-items {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-content strong {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-question-info {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pack-import-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-questions-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pack-import-confirm-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { questionsApi } from '../services/api'
|
||||||
import './QuestionsModal.css'
|
import './QuestionsModal.css'
|
||||||
|
|
||||||
const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
|
const QuestionsModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
questions,
|
||||||
|
onUpdateQuestions,
|
||||||
|
isOnlineMode = false,
|
||||||
|
roomId = null,
|
||||||
|
availablePacks = [],
|
||||||
|
}) => {
|
||||||
const [editingQuestion, setEditingQuestion] = useState(null)
|
const [editingQuestion, setEditingQuestion] = useState(null)
|
||||||
const [questionText, setQuestionText] = useState('')
|
const [questionText, setQuestionText] = useState('')
|
||||||
const [answers, setAnswers] = useState([
|
const [answers, setAnswers] = useState([
|
||||||
|
|
@ -13,6 +22,11 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
|
||||||
{ text: '', points: 10 },
|
{ text: '', points: 10 },
|
||||||
])
|
])
|
||||||
const [jsonError, setJsonError] = useState('')
|
const [jsonError, setJsonError] = useState('')
|
||||||
|
const [showPackImport, setShowPackImport] = useState(false)
|
||||||
|
const [selectedPack, setSelectedPack] = useState(null)
|
||||||
|
const [packQuestions, setPackQuestions] = useState([])
|
||||||
|
const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set())
|
||||||
|
const [savingToRoom, setSavingToRoom] = useState(false)
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
|
@ -189,6 +203,55 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
|
||||||
input.click()
|
input.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectPack = async (packId) => {
|
||||||
|
if (!packId) {
|
||||||
|
setPackQuestions([])
|
||||||
|
setSelectedPack(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await questionsApi.getPack(packId)
|
||||||
|
setPackQuestions(response.data.questions || [])
|
||||||
|
setSelectedPack(packId)
|
||||||
|
setSelectedQuestionIndices(new Set())
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching pack:', error)
|
||||||
|
setJsonError('Ошибка загрузки пака вопросов')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleQuestion = (index) => {
|
||||||
|
const newSelected = new Set(selectedQuestionIndices)
|
||||||
|
if (newSelected.has(index)) {
|
||||||
|
newSelected.delete(index)
|
||||||
|
} else {
|
||||||
|
newSelected.add(index)
|
||||||
|
}
|
||||||
|
setSelectedQuestionIndices(newSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportSelected = () => {
|
||||||
|
const indices = Array.from(selectedQuestionIndices)
|
||||||
|
const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean)
|
||||||
|
|
||||||
|
// Create deep copies
|
||||||
|
const copiedQuestions = questionsToImport.map(q => ({
|
||||||
|
id: Date.now() + Math.random(), // Generate new ID
|
||||||
|
text: q.text,
|
||||||
|
answers: q.answers.map(a => ({ text: a.text, points: a.points })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const updatedQuestions = [...questions, ...copiedQuestions]
|
||||||
|
onUpdateQuestions(updatedQuestions)
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
setSelectedQuestionIndices(new Set())
|
||||||
|
setShowPackImport(false)
|
||||||
|
setJsonError('')
|
||||||
|
alert(`Импортировано ${copiedQuestions.length} вопросов`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="questions-modal-backdrop" onClick={handleBackdropClick}>
|
<div className="questions-modal-backdrop" onClick={handleBackdropClick}>
|
||||||
<div className="questions-modal-content">
|
<div className="questions-modal-content">
|
||||||
|
|
@ -212,12 +275,71 @@ const QuestionsModal = ({ isOpen, onClose, questions, onUpdateQuestions }) => {
|
||||||
>
|
>
|
||||||
📤 Импорт JSON
|
📤 Импорт JSON
|
||||||
</button>
|
</button>
|
||||||
|
{availablePacks.length > 0 && (
|
||||||
|
<button
|
||||||
|
className="questions-modal-pack-import-button"
|
||||||
|
onClick={() => setShowPackImport(!showPackImport)}
|
||||||
|
>
|
||||||
|
📦 {showPackImport ? 'Скрыть импорт' : 'Импорт из пака'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{jsonError && (
|
{jsonError && (
|
||||||
<div className="questions-modal-error">{jsonError}</div>
|
<div className="questions-modal-error">{jsonError}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPackImport && availablePacks.length > 0 && (
|
||||||
|
<div className="pack-import-section">
|
||||||
|
<h3>Импорт вопросов из пака</h3>
|
||||||
|
<select
|
||||||
|
value={selectedPack || ''}
|
||||||
|
onChange={(e) => handleSelectPack(e.target.value)}
|
||||||
|
className="pack-import-select"
|
||||||
|
>
|
||||||
|
<option value="">-- Выберите пак --</option>
|
||||||
|
{availablePacks.map(pack => (
|
||||||
|
<option key={pack.id} value={pack.id}>
|
||||||
|
{pack.name} ({pack.questionCount} вопросов)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{packQuestions.length > 0 && (
|
||||||
|
<div className="pack-questions-list">
|
||||||
|
<div className="pack-questions-header">
|
||||||
|
<span>Выберите вопросы для импорта:</span>
|
||||||
|
<button
|
||||||
|
onClick={handleImportSelected}
|
||||||
|
disabled={selectedQuestionIndices.size === 0}
|
||||||
|
className="pack-import-confirm-button"
|
||||||
|
>
|
||||||
|
Импортировать ({selectedQuestionIndices.size})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pack-questions-items">
|
||||||
|
{packQuestions.map((q, idx) => (
|
||||||
|
<div key={idx} className="pack-question-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedQuestionIndices.has(idx)}
|
||||||
|
onChange={() => handleToggleQuestion(idx)}
|
||||||
|
/>
|
||||||
|
<div className="pack-question-content">
|
||||||
|
<strong>{q.text}</strong>
|
||||||
|
<span className="pack-question-info">
|
||||||
|
{q.answers.length} ответов
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="questions-modal-form">
|
<div className="questions-modal-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { roomsApi } from '../services/api';
|
import { roomsApi } from '../services/api';
|
||||||
import socketService from '../services/socket';
|
import socketService from '../services/socket';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
export const useRoom = (roomCode, onGameStarted = null) => {
|
export const useRoom = (roomCode, onGameStarted = null) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const [room, setRoom] = useState(null);
|
const [room, setRoom] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
@ -35,7 +37,7 @@ export const useRoom = (roomCode, onGameStarted = null) => {
|
||||||
socketService.connect();
|
socketService.connect();
|
||||||
|
|
||||||
// Join the room via WebSocket
|
// Join the room via WebSocket
|
||||||
socketService.joinRoom(roomCode);
|
socketService.joinRoom(roomCode, user?.id);
|
||||||
|
|
||||||
// Listen for room updates
|
// Listen for room updates
|
||||||
const handleRoomUpdate = (updatedRoom) => {
|
const handleRoomUpdate = (updatedRoom) => {
|
||||||
|
|
@ -75,12 +77,18 @@ export const useRoom = (roomCode, onGameStarted = null) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRoomPackUpdated = (updatedRoom) => {
|
||||||
|
console.log('Room pack updated:', updatedRoom);
|
||||||
|
setRoom(updatedRoom);
|
||||||
|
};
|
||||||
|
|
||||||
socketService.on('roomUpdate', handleRoomUpdate);
|
socketService.on('roomUpdate', handleRoomUpdate);
|
||||||
socketService.on('gameStarted', handleGameStarted);
|
socketService.on('gameStarted', handleGameStarted);
|
||||||
socketService.on('answerRevealed', handleAnswerRevealed);
|
socketService.on('answerRevealed', handleAnswerRevealed);
|
||||||
socketService.on('scoreUpdated', handleScoreUpdated);
|
socketService.on('scoreUpdated', handleScoreUpdated);
|
||||||
socketService.on('questionChanged', handleQuestionChanged);
|
socketService.on('questionChanged', handleQuestionChanged);
|
||||||
socketService.on('gameEnded', handleGameEnded);
|
socketService.on('gameEnded', handleGameEnded);
|
||||||
|
socketService.on('roomPackUpdated', handleRoomPackUpdated);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socketService.off('roomUpdate', handleRoomUpdate);
|
socketService.off('roomUpdate', handleRoomUpdate);
|
||||||
|
|
@ -89,8 +97,9 @@ export const useRoom = (roomCode, onGameStarted = null) => {
|
||||||
socketService.off('scoreUpdated', handleScoreUpdated);
|
socketService.off('scoreUpdated', handleScoreUpdated);
|
||||||
socketService.off('questionChanged', handleQuestionChanged);
|
socketService.off('questionChanged', handleQuestionChanged);
|
||||||
socketService.off('gameEnded', handleGameEnded);
|
socketService.off('gameEnded', handleGameEnded);
|
||||||
|
socketService.off('roomPackUpdated', handleRoomPackUpdated);
|
||||||
};
|
};
|
||||||
}, [roomCode, onGameStarted]);
|
}, [roomCode, onGameStarted, room]);
|
||||||
|
|
||||||
const createRoom = useCallback(async (hostId, questionPackId, settings = {}) => {
|
const createRoom = useCallback(async (hostId, questionPackId, settings = {}) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -114,34 +123,34 @@ export const useRoom = (roomCode, onGameStarted = null) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startGame = useCallback(() => {
|
const startGame = useCallback(() => {
|
||||||
if (room) {
|
if (room && user) {
|
||||||
socketService.startGame(room.id, room.code);
|
socketService.startGame(room.id, room.code, user.id);
|
||||||
}
|
}
|
||||||
}, [room]);
|
}, [room, user]);
|
||||||
|
|
||||||
const revealAnswer = useCallback((answerIndex) => {
|
const revealAnswer = useCallback((answerIndex) => {
|
||||||
if (room) {
|
if (room && user) {
|
||||||
socketService.revealAnswer(room.code, answerIndex);
|
socketService.revealAnswer(room.code, room.id, user.id, answerIndex);
|
||||||
}
|
}
|
||||||
}, [room]);
|
}, [room, user]);
|
||||||
|
|
||||||
const updateScore = useCallback((participantId, score) => {
|
const updateScore = useCallback((participantId, score) => {
|
||||||
if (room) {
|
if (room && user) {
|
||||||
socketService.updateScore(participantId, score, room.code);
|
socketService.updateScore(participantId, score, room.code, room.id, user.id);
|
||||||
}
|
}
|
||||||
}, [room]);
|
}, [room, user]);
|
||||||
|
|
||||||
const nextQuestion = useCallback(() => {
|
const nextQuestion = useCallback(() => {
|
||||||
if (room) {
|
if (room && user) {
|
||||||
socketService.nextQuestion(room.code);
|
socketService.nextQuestion(room.code, room.id, user.id);
|
||||||
}
|
}
|
||||||
}, [room]);
|
}, [room, user]);
|
||||||
|
|
||||||
const endGame = useCallback(() => {
|
const endGame = useCallback(() => {
|
||||||
if (room) {
|
if (room && user) {
|
||||||
socketService.endGame(room.id, room.code);
|
socketService.endGame(room.id, room.code, user.id);
|
||||||
}
|
}
|
||||||
}, [room]);
|
}, [room, user]);
|
||||||
|
|
||||||
const updateQuestionPack = useCallback(
|
const updateQuestionPack = useCallback(
|
||||||
async (questionPackId) => {
|
async (questionPackId) => {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,35 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.host-controls-inline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-questions-button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-questions-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-questions-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pack-selector-inline {
|
.pack-selector-inline {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useRoom } from '../hooks/useRoom';
|
import { useRoom } from '../hooks/useRoom';
|
||||||
import { questionsApi } from '../services/api';
|
import { questionsApi, roomsApi } from '../services/api';
|
||||||
import Game from '../components/Game';
|
import Game from '../components/Game';
|
||||||
|
import QuestionsModal from '../components/QuestionsModal';
|
||||||
import './GamePage.css';
|
import './GamePage.css';
|
||||||
|
|
||||||
const GamePage = () => {
|
const GamePage = () => {
|
||||||
|
|
@ -24,6 +25,7 @@ const GamePage = () => {
|
||||||
const [questionPacks, setQuestionPacks] = useState([]);
|
const [questionPacks, setQuestionPacks] = useState([]);
|
||||||
const [selectedPackId, setSelectedPackId] = useState('');
|
const [selectedPackId, setSelectedPackId] = useState('');
|
||||||
const [updatingPack, setUpdatingPack] = useState(false);
|
const [updatingPack, setUpdatingPack] = useState(false);
|
||||||
|
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadQuestions = async () => {
|
const loadQuestions = async () => {
|
||||||
|
|
@ -31,32 +33,28 @@ const GamePage = () => {
|
||||||
|
|
||||||
setLoadingQuestions(true);
|
setLoadingQuestions(true);
|
||||||
try {
|
try {
|
||||||
|
// Load from roomPack (always exists now)
|
||||||
|
if (room.roomPack) {
|
||||||
|
const questions = room.roomPack.questions;
|
||||||
|
setQuestions(Array.isArray(questions) ? questions : []);
|
||||||
|
} else {
|
||||||
|
// Fallback for legacy rooms without roomPack
|
||||||
if (room.questionPackId) {
|
if (room.questionPackId) {
|
||||||
// Загружаем вопросы из пака
|
|
||||||
if (room.questionPack && room.questionPack.questions) {
|
if (room.questionPack && room.questionPack.questions) {
|
||||||
const packQuestions = room.questionPack.questions;
|
const packQuestions = room.questionPack.questions;
|
||||||
if (Array.isArray(packQuestions)) {
|
setQuestions(Array.isArray(packQuestions) ? packQuestions : []);
|
||||||
setQuestions(packQuestions);
|
|
||||||
} else {
|
} else {
|
||||||
setQuestions([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Загружаем пак отдельно, если он не включен в room
|
|
||||||
const response = await questionsApi.getPack(room.questionPackId);
|
const response = await questionsApi.getPack(room.questionPackId);
|
||||||
if (response.data && response.data.questions) {
|
|
||||||
setQuestions(
|
setQuestions(
|
||||||
Array.isArray(response.data.questions)
|
response.data?.questions && Array.isArray(response.data.questions)
|
||||||
? response.data.questions
|
? response.data.questions
|
||||||
: [],
|
: []
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setQuestions([]);
|
setQuestions([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Пак не выбран, начинаем с пустого списка вопросов
|
|
||||||
setQuestions([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading questions:', error);
|
console.error('Error loading questions:', error);
|
||||||
setQuestions([]);
|
setQuestions([]);
|
||||||
|
|
@ -150,61 +148,43 @@ const GamePage = () => {
|
||||||
|
|
||||||
const isHost = user && room.hostId === user.id;
|
const isHost = user && room.hostId === user.id;
|
||||||
|
|
||||||
|
const handleUpdateRoomQuestions = async (newQuestions) => {
|
||||||
|
setQuestions(newQuestions);
|
||||||
|
if (room) {
|
||||||
|
try {
|
||||||
|
await roomsApi.updateRoomPack(room.id, newQuestions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating room pack:', error);
|
||||||
|
alert('Ошибка при сохранении вопросов');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-page">
|
<div className="game-page">
|
||||||
{isHost && (
|
|
||||||
<div className="host-controls">
|
|
||||||
<div className="pack-selector-inline">
|
|
||||||
<label>Пак вопросов:</label>
|
|
||||||
<select
|
|
||||||
value={selectedPackId}
|
|
||||||
onChange={(e) => setSelectedPackId(e.target.value)}
|
|
||||||
disabled={updatingPack}
|
|
||||||
>
|
|
||||||
<option value="">
|
|
||||||
{room.questionPackId
|
|
||||||
? 'Изменить пак вопросов'
|
|
||||||
: 'Выберите пак вопросов'}
|
|
||||||
</option>
|
|
||||||
{questionPacks.map((pack) => (
|
|
||||||
<option key={pack.id} value={pack.id}>
|
|
||||||
{pack.name} ({pack.questionCount} вопросов)
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
onClick={handleUpdateQuestionPack}
|
|
||||||
disabled={
|
|
||||||
!selectedPackId ||
|
|
||||||
selectedPackId === room.questionPackId ||
|
|
||||||
updatingPack
|
|
||||||
}
|
|
||||||
className="secondary"
|
|
||||||
>
|
|
||||||
{updatingPack ? 'Сохранение...' : 'Применить'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(`/room/${roomCode}`)}
|
|
||||||
className="secondary"
|
|
||||||
>
|
|
||||||
Назад в комнату
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="game-container">
|
<div className="game-container">
|
||||||
{questions.length === 0 && (
|
{questions.length === 0 && (
|
||||||
<div className="no-questions-banner">
|
<div className="no-questions-banner">
|
||||||
<p>
|
<p>
|
||||||
Вопросы не загружены.
|
Вопросы не загружены.
|
||||||
{isHost
|
{isHost
|
||||||
? ' Выберите пак вопросов выше, чтобы начать игру.'
|
? ' Откройте управление вопросами, чтобы добавить вопросы.'
|
||||||
: ' Ожидайте, пока ведущий добавит вопросы.'}
|
: ' Ожидайте, пока ведущий добавит вопросы.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isHost && (
|
||||||
|
<div className="host-controls-inline">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsQuestionsModalOpen(true)}
|
||||||
|
className="manage-questions-button"
|
||||||
|
>
|
||||||
|
Управление вопросами
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Game
|
<Game
|
||||||
questions={questions}
|
questions={questions}
|
||||||
currentQuestionIndex={currentQuestionIndex}
|
currentQuestionIndex={currentQuestionIndex}
|
||||||
|
|
@ -214,6 +194,18 @@ const GamePage = () => {
|
||||||
isOnlineMode={true}
|
isOnlineMode={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isHost && (
|
||||||
|
<QuestionsModal
|
||||||
|
isOpen={isQuestionsModalOpen}
|
||||||
|
onClose={() => setIsQuestionsModalOpen(false)}
|
||||||
|
questions={questions}
|
||||||
|
onUpdateQuestions={handleUpdateRoomQuestions}
|
||||||
|
isOnlineMode={true}
|
||||||
|
roomId={room?.id}
|
||||||
|
availablePacks={questionPacks}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ export const roomsApi = {
|
||||||
api.post(`/rooms/${roomId}/join`, { userId, name, role }),
|
api.post(`/rooms/${roomId}/join`, { userId, name, role }),
|
||||||
updateQuestionPack: (roomId, questionPackId) =>
|
updateQuestionPack: (roomId, questionPackId) =>
|
||||||
api.patch(`/rooms/${roomId}/question-pack`, { questionPackId }),
|
api.patch(`/rooms/${roomId}/question-pack`, { questionPackId }),
|
||||||
|
updateRoomPack: (roomId, questions) =>
|
||||||
|
api.patch(`/rooms/${roomId}/room-pack`, { questions }),
|
||||||
|
getRoomPack: (roomId) =>
|
||||||
|
api.get(`/rooms/${roomId}/room-pack`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Questions endpoints
|
// Questions endpoints
|
||||||
|
|
|
||||||
|
|
@ -78,24 +78,38 @@ class SocketService {
|
||||||
this.emit('joinRoom', { roomCode, userId });
|
this.emit('joinRoom', { roomCode, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
startGame(roomId, roomCode) {
|
startGame(roomId, roomCode, userId) {
|
||||||
this.emit('startGame', { roomId, roomCode });
|
this.emit('startGame', { roomId, roomCode, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
revealAnswer(roomCode, answerIndex) {
|
revealAnswer(roomCode, roomId, userId, answerIndex) {
|
||||||
this.emit('revealAnswer', { roomCode, answerIndex });
|
this.emit('revealAnswer', { roomCode, roomId, userId, answerIndex });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScore(participantId, score, roomCode) {
|
updateScore(participantId, score, roomCode, roomId, userId) {
|
||||||
this.emit('updateScore', { participantId, score, roomCode });
|
this.emit('updateScore', { participantId, score, roomCode, roomId, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
nextQuestion(roomCode) {
|
nextQuestion(roomCode, roomId, userId) {
|
||||||
this.emit('nextQuestion', { roomCode });
|
this.emit('nextQuestion', { roomCode, roomId, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
endGame(roomId, roomCode) {
|
endGame(roomId, roomCode, userId) {
|
||||||
this.emit('endGame', { roomId, roomCode });
|
this.emit('endGame', { roomId, roomCode, userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRoomPack(roomId, roomCode, userId, questions) {
|
||||||
|
this.emit('updateRoomPack', { roomId, roomCode, userId, questions });
|
||||||
|
}
|
||||||
|
|
||||||
|
importQuestions(roomId, roomCode, userId, sourcePackId, questionIndices) {
|
||||||
|
this.emit('importQuestions', {
|
||||||
|
roomId,
|
||||||
|
roomCode,
|
||||||
|
userId,
|
||||||
|
sourcePackId,
|
||||||
|
questionIndices,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue