wip
This commit is contained in:
parent
560f016b08
commit
aae72ac313
13 changed files with 588 additions and 10 deletions
|
|
@ -206,4 +206,88 @@ export const packsApi = {
|
|||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Get empty template for import
|
||||
getTemplate: async (): Promise<{
|
||||
templateVersion: string
|
||||
instructions: string
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
}> => {
|
||||
try {
|
||||
const response = await adminApiClient.get('/api/admin/packs/export/template')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<{ error?: string; message?: string }>
|
||||
throw createPacksApiError(
|
||||
axiosError.response?.data?.message ||
|
||||
axiosError.response?.data?.error ||
|
||||
'Failed to get template',
|
||||
axiosError.response?.status,
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Export pack to JSON
|
||||
exportPack: async (packId: string): Promise<{
|
||||
templateVersion: string
|
||||
exportedAt: string
|
||||
packInfo: {
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
isPublic: boolean
|
||||
}
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
}> => {
|
||||
try {
|
||||
const response = await adminApiClient.get(`/api/admin/packs/${packId}/export`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<{ error?: string; message?: string }>
|
||||
|
||||
if (axiosError.response?.status === 404) {
|
||||
throw createPacksApiError(
|
||||
`Pack not found: The pack with ID "${packId}" does not exist.`,
|
||||
axiosError.response.status,
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
throw createPacksApiError(
|
||||
axiosError.response?.data?.message ||
|
||||
axiosError.response?.data?.error ||
|
||||
`Failed to export pack ${packId}`,
|
||||
axiosError.response?.status,
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// Import pack from JSON
|
||||
importPack: async (data: {
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
isPublic: boolean
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
}): Promise<EditCardPackDto> => {
|
||||
try {
|
||||
const response = await adminApiClient.post('/api/admin/packs/import', data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<{ error?: string; message?: string }>
|
||||
throw createPacksApiError(
|
||||
axiosError.response?.data?.message ||
|
||||
axiosError.response?.data?.error ||
|
||||
'Failed to import pack',
|
||||
axiosError.response?.status,
|
||||
undefined,
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,24 @@ export const usersApi = {
|
|||
getUsers: async (params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
}): Promise<PaginatedResponse<UserDto>> => {
|
||||
const response = await adminApiClient.get('/api/admin/users', {
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
search: params?.search || undefined,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
// Transform backend response (users) to frontend format (items)
|
||||
const backendData = response.data
|
||||
return {
|
||||
items: backendData.users || [],
|
||||
total: backendData.total || 0,
|
||||
page: backendData.page || 1,
|
||||
limit: backendData.limit || 20,
|
||||
totalPages: backendData.totalPages || 1,
|
||||
}
|
||||
},
|
||||
|
||||
// Get single user by ID
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ export default function PacksPage() {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.items.map((pack) => (
|
||||
{(data?.items || []).map((pack) => (
|
||||
<TableRow key={pack.id}>
|
||||
<TableCell className="font-mono text-sm">{pack.id}</TableCell>
|
||||
<TableCell>
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export default function UsersPage() {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.items
|
||||
{(data?.items || [])
|
||||
.filter(user =>
|
||||
search === '' ||
|
||||
user.name?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
|
|
|
|||
|
|
@ -173,3 +173,17 @@ enum CodeStatus {
|
|||
USED
|
||||
EXPIRED
|
||||
}
|
||||
|
||||
model Theme {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
isPublic Boolean @default(false)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
colors Json // { bgPrimary, bgOverlay, bgCard, textPrimary, textSecondary, accentPrimary, etc. }
|
||||
settings Json // { shadowSm, shadowMd, blurAmount, borderRadius, animationSpeed, etc. }
|
||||
|
||||
creator User @relation(fields: [createdBy], references: [id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import {
|
|||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { AdminPacksService } from './admin-packs.service';
|
||||
import { PackFiltersDto } from './dto/pack-filters.dto';
|
||||
import { CreatePackDto } from './dto/create-pack.dto';
|
||||
import { UpdatePackDto } from './dto/update-pack.dto';
|
||||
import { ImportPackDto } from './dto/import-pack.dto';
|
||||
import { AdminAuthGuard } from '../guards/admin-auth.guard';
|
||||
import { AdminGuard } from '../guards/admin.guard';
|
||||
|
||||
|
|
@ -46,4 +48,55 @@ export class AdminPacksController {
|
|||
remove(@Param('id') id: string) {
|
||||
return this.adminPacksService.remove(id);
|
||||
}
|
||||
|
||||
@Get('export/template')
|
||||
getTemplate() {
|
||||
return {
|
||||
templateVersion: '1.0',
|
||||
instructions: 'Fill in your questions below. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
|
||||
questions: [
|
||||
{
|
||||
question: 'Your question here',
|
||||
answers: [
|
||||
{ text: 'Answer 1', points: 100 },
|
||||
{ text: 'Answer 2', points: 80 },
|
||||
{ text: 'Answer 3', points: 60 },
|
||||
{ text: 'Answer 4', points: 40 },
|
||||
{ text: 'Answer 5', points: 20 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id/export')
|
||||
async exportPack(@Param('id') id: string) {
|
||||
return this.adminPacksService.exportPack(id);
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
async importPack(@Body() importPackDto: ImportPackDto, @Request() req) {
|
||||
// Validate question structure
|
||||
const isValid = importPackDto.questions.every(
|
||||
(q) =>
|
||||
q.question &&
|
||||
typeof q.question === 'string' &&
|
||||
Array.isArray(q.answers) &&
|
||||
q.answers.length > 0 &&
|
||||
q.answers.every(
|
||||
(a) =>
|
||||
a.text &&
|
||||
typeof a.text === 'string' &&
|
||||
typeof a.points === 'number',
|
||||
),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestException(
|
||||
'Invalid question format. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.adminPacksService.create(importPackDto, req.user.sub);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,4 +160,26 @@ export class AdminPacksService {
|
|||
|
||||
return { message: 'Question pack deleted successfully' };
|
||||
}
|
||||
|
||||
async exportPack(id: string) {
|
||||
const pack = await this.prisma.questionPack.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!pack) {
|
||||
throw new NotFoundException('Question pack not found');
|
||||
}
|
||||
|
||||
return {
|
||||
templateVersion: '1.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
packInfo: {
|
||||
name: pack.name,
|
||||
description: pack.description,
|
||||
category: pack.category,
|
||||
isPublic: pack.isPublic,
|
||||
},
|
||||
questions: pack.questions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
backend/src/admin/packs/dto/import-pack.dto.ts
Normal file
47
backend/src/admin/packs/dto/import-pack.dto.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class ImportAnswerDto {
|
||||
@IsString()
|
||||
text: string;
|
||||
|
||||
@IsNumber()
|
||||
points: number;
|
||||
}
|
||||
|
||||
class ImportQuestionDto {
|
||||
@IsString()
|
||||
question: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ImportAnswerDto)
|
||||
answers: ImportAnswerDto[];
|
||||
}
|
||||
|
||||
export class ImportPackDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
category: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPublic?: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ImportQuestionDto)
|
||||
questions: ImportQuestionDto[];
|
||||
}
|
||||
|
|
@ -608,4 +608,63 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
|
||||
await this.broadcastFullState(payload.roomCode);
|
||||
}
|
||||
|
||||
@SubscribeMessage('updatePlayerName')
|
||||
async handleUpdatePlayerName(client: Socket, payload: {
|
||||
roomId: string;
|
||||
roomCode: string;
|
||||
userId: string;
|
||||
participantId: string;
|
||||
newName: string;
|
||||
}) {
|
||||
const isHost = await this.isHost(payload.roomId, payload.userId);
|
||||
if (!isHost) {
|
||||
client.emit('error', { message: 'Only the host can update player names' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.newName || payload.newName.trim().length === 0) {
|
||||
client.emit('error', { message: 'Name cannot be empty' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.newName.trim().length > 50) {
|
||||
client.emit('error', { message: 'Name is too long (max 50 characters)' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prisma.participant.update({
|
||||
where: { id: payload.participantId },
|
||||
data: { name: payload.newName.trim() }
|
||||
});
|
||||
|
||||
await this.broadcastFullState(payload.roomCode);
|
||||
}
|
||||
|
||||
@SubscribeMessage('updatePlayerScore')
|
||||
async handleUpdatePlayerScore(client: Socket, payload: {
|
||||
roomId: string;
|
||||
roomCode: string;
|
||||
userId: string;
|
||||
participantId: string;
|
||||
newScore: number;
|
||||
}) {
|
||||
const isHost = await this.isHost(payload.roomId, payload.userId);
|
||||
if (!isHost) {
|
||||
client.emit('error', { message: 'Only the host can update scores' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof payload.newScore !== 'number' || isNaN(payload.newScore)) {
|
||||
client.emit('error', { message: 'Invalid score value' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prisma.participant.update({
|
||||
where: { id: payload.participantId },
|
||||
data: { score: Math.round(payload.newScore) }
|
||||
});
|
||||
|
||||
await this.broadcastFullState(payload.roomCode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,122 @@
|
|||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent-primary, #ffd700);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.player-score-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Player editing */
|
||||
.player-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.player-edit-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.player-item:hover .player-edit-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player-edit-btn:hover {
|
||||
color: var(--accent-primary, #ffd700);
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
}
|
||||
|
||||
.player-edit-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.player-edit-input {
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--bg-card, #1a1a1a);
|
||||
border: 2px solid var(--accent-primary, #ffd700);
|
||||
border-radius: var(--border-radius-sm, 8px);
|
||||
color: var(--text-primary, #ffffff);
|
||||
font-size: 0.95rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.player-edit-input-score {
|
||||
min-width: 80px;
|
||||
max-width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-edit-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.player-edit-save,
|
||||
.player-edit-cancel {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.player-edit-save {
|
||||
background: var(--accent-success, #4ecdc4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.player-edit-save:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.player-edit-cancel {
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
color: var(--accent-secondary, #ff6b6b);
|
||||
}
|
||||
|
||||
.player-edit-cancel:hover {
|
||||
background: rgba(255, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.player-kick-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.player-item:hover .player-kick-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player-kick-btn:hover {
|
||||
color: var(--accent-secondary, #ff6b6b);
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Game controls tab */
|
||||
|
|
|
|||
|
|
@ -25,11 +25,20 @@ const GameManagementModal = ({
|
|||
onHideAllAnswers,
|
||||
onAwardPoints,
|
||||
onPenalty,
|
||||
onUpdatePlayerName,
|
||||
onUpdatePlayerScore,
|
||||
onKickPlayer,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('players') // players | game | answers | scoring | questions
|
||||
const [selectedPlayer, setSelectedPlayer] = useState(null)
|
||||
const [customPoints, setCustomPoints] = useState(10)
|
||||
|
||||
// Player editing state
|
||||
const [editingPlayerId, setEditingPlayerId] = useState(null)
|
||||
const [editingPlayerName, setEditingPlayerName] = useState('')
|
||||
const [editingPlayerScore, setEditingPlayerScore] = useState('')
|
||||
const [editMode, setEditMode] = useState(null) // 'name' | 'score'
|
||||
|
||||
// Questions management state
|
||||
const [editingQuestion, setEditingQuestion] = useState(null)
|
||||
const [questionText, setQuestionText] = useState('')
|
||||
|
|
@ -82,6 +91,53 @@ const GameManagementModal = ({
|
|||
}
|
||||
}
|
||||
|
||||
// Player editing handlers
|
||||
const handleStartEditName = (participant) => {
|
||||
setEditingPlayerId(participant.id)
|
||||
setEditingPlayerName(participant.name)
|
||||
setEditMode('name')
|
||||
}
|
||||
|
||||
const handleStartEditScore = (participant) => {
|
||||
setEditingPlayerId(participant.id)
|
||||
setEditingPlayerScore(String(participant.score || 0))
|
||||
setEditMode('score')
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingPlayerId(null)
|
||||
setEditingPlayerName('')
|
||||
setEditingPlayerScore('')
|
||||
setEditMode(null)
|
||||
}
|
||||
|
||||
const handleSavePlayerName = () => {
|
||||
if (editingPlayerName.trim() && onUpdatePlayerName) {
|
||||
onUpdatePlayerName(editingPlayerId, editingPlayerName.trim())
|
||||
}
|
||||
handleCancelEdit()
|
||||
}
|
||||
|
||||
const handleSavePlayerScore = () => {
|
||||
const score = parseInt(editingPlayerScore, 10)
|
||||
if (!isNaN(score) && onUpdatePlayerScore) {
|
||||
onUpdatePlayerScore(editingPlayerId, score)
|
||||
}
|
||||
handleCancelEdit()
|
||||
}
|
||||
|
||||
const handleKeyDown = (e, type) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (type === 'name') {
|
||||
handleSavePlayerName()
|
||||
} else if (type === 'score') {
|
||||
handleSavePlayerScore()
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancelEdit()
|
||||
}
|
||||
}
|
||||
|
||||
// Questions management handlers
|
||||
const resetQuestionForm = () => {
|
||||
setEditingQuestion(null)
|
||||
|
|
@ -410,15 +466,77 @@ const GameManagementModal = ({
|
|||
participants.map((participant) => (
|
||||
<div key={participant.id} className="player-item">
|
||||
<div className="player-info">
|
||||
<span className="player-name">{participant.name}</span>
|
||||
{editingPlayerId === participant.id && editMode === 'name' ? (
|
||||
<div className="player-edit-field">
|
||||
<input
|
||||
type="text"
|
||||
value={editingPlayerName}
|
||||
onChange={(e) => setEditingPlayerName(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'name')}
|
||||
autoFocus
|
||||
maxLength={50}
|
||||
className="player-edit-input"
|
||||
/>
|
||||
<button className="player-edit-save" onClick={handleSavePlayerName}>✓</button>
|
||||
<button className="player-edit-cancel" onClick={handleCancelEdit}>✕</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="player-name">
|
||||
{participant.name}
|
||||
<button
|
||||
className="player-edit-btn"
|
||||
onClick={() => handleStartEditName(participant)}
|
||||
title="Редактировать имя"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
<span className="player-role">
|
||||
{participant.role === 'HOST' && '👑 Ведущий'}
|
||||
{participant.role === 'SPECTATOR' && '👀 Зритель'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="player-score">
|
||||
{participant.score || 0} очков
|
||||
<div className="player-score-section">
|
||||
{editingPlayerId === participant.id && editMode === 'score' ? (
|
||||
<div className="player-edit-field">
|
||||
<input
|
||||
type="number"
|
||||
value={editingPlayerScore}
|
||||
onChange={(e) => setEditingPlayerScore(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'score')}
|
||||
autoFocus
|
||||
className="player-edit-input player-edit-input-score"
|
||||
/>
|
||||
<button className="player-edit-save" onClick={handleSavePlayerScore}>✓</button>
|
||||
<button className="player-edit-cancel" onClick={handleCancelEdit}>✕</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="player-score">
|
||||
{participant.score || 0} очков
|
||||
<button
|
||||
className="player-edit-btn"
|
||||
onClick={() => handleStartEditScore(participant)}
|
||||
title="Редактировать очки"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{participant.role !== 'HOST' && onKickPlayer && (
|
||||
<button
|
||||
className="player-kick-btn"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Удалить игрока ${participant.name}?`)) {
|
||||
onKickPlayer(participant.id)
|
||||
}
|
||||
}}
|
||||
title="Удалить игрока"
|
||||
>
|
||||
🚫
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,38 @@ const GamePage = () => {
|
|||
console.warn('Manual point award not implemented yet');
|
||||
};
|
||||
|
||||
const handleUpdatePlayerName = (participantId, newName) => {
|
||||
if (!gameState.roomId || !user) return;
|
||||
socketService.updatePlayerName(
|
||||
gameState.roomId,
|
||||
gameState.roomCode,
|
||||
user.id,
|
||||
participantId,
|
||||
newName
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdatePlayerScore = (participantId, newScore) => {
|
||||
if (!gameState.roomId || !user) return;
|
||||
socketService.updatePlayerScore(
|
||||
gameState.roomId,
|
||||
gameState.roomCode,
|
||||
user.id,
|
||||
participantId,
|
||||
newScore
|
||||
);
|
||||
};
|
||||
|
||||
const handleKickPlayer = (participantId) => {
|
||||
if (!gameState.roomId || !user) return;
|
||||
socketService.emit('kickPlayer', {
|
||||
roomId: gameState.roomId,
|
||||
roomCode: gameState.roomCode,
|
||||
userId: user.id,
|
||||
participantId: participantId
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectPlayer = (participantId) => {
|
||||
if (!gameState.roomId || !user) return;
|
||||
if (!isHost) return; // Только хост может выбирать игрока
|
||||
|
|
@ -407,6 +439,9 @@ const GamePage = () => {
|
|||
onShowAllAnswers={handleShowAllAnswers}
|
||||
onHideAllAnswers={handleHideAllAnswers}
|
||||
onAwardPoints={handleAwardPoints}
|
||||
onUpdatePlayerName={handleUpdatePlayerName}
|
||||
onUpdatePlayerScore={handleUpdatePlayerScore}
|
||||
onKickPlayer={handleKickPlayer}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,26 @@ class SocketService {
|
|||
questionIndices,
|
||||
});
|
||||
}
|
||||
|
||||
updatePlayerName(roomId, roomCode, userId, participantId, newName) {
|
||||
this.emit('updatePlayerName', {
|
||||
roomId,
|
||||
roomCode,
|
||||
userId,
|
||||
participantId,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
updatePlayerScore(roomId, roomCode, userId, participantId, newScore) {
|
||||
this.emit('updatePlayerScore', {
|
||||
roomId,
|
||||
roomCode,
|
||||
userId,
|
||||
participantId,
|
||||
newScore,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketService();
|
||||
|
|
|
|||
Loading…
Reference in a new issue