1144 lines
44 KiB
JavaScript
1144 lines
44 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
import { questionsApi } from '../services/api'
|
||
import { useTheme } from '../context/ThemeContext'
|
||
import './GameManagementModal.css'
|
||
import './QuestionsModal.css'
|
||
|
||
const GameManagementModal = ({
|
||
isOpen,
|
||
onClose,
|
||
room,
|
||
participants,
|
||
currentQuestion,
|
||
currentQuestionIndex,
|
||
totalQuestions,
|
||
revealedAnswers,
|
||
questions = [],
|
||
onUpdateQuestions,
|
||
availablePacks = [],
|
||
onStartGame,
|
||
onEndGame,
|
||
onNextQuestion,
|
||
onPreviousQuestion,
|
||
onRevealAnswer,
|
||
onHideAnswer,
|
||
onShowAllAnswers,
|
||
onHideAllAnswers,
|
||
onAwardPoints,
|
||
onPenalty,
|
||
onUpdatePlayerName,
|
||
onUpdatePlayerScore,
|
||
onKickPlayer,
|
||
onChangeParticipantRole,
|
||
particlesEnabled = null,
|
||
onToggleParticles,
|
||
initialTab = 'players',
|
||
}) => {
|
||
const { currentThemeData } = useTheme()
|
||
const [activeTab, setActiveTab] = useState(initialTab) // players | game | scoring | questions
|
||
const [selectedPlayer, setSelectedPlayer] = useState(null)
|
||
const [customPoints, setCustomPoints] = useState(10)
|
||
|
||
// Determine actual particles enabled state (room override or theme default)
|
||
const getActualParticlesEnabled = () => {
|
||
if (particlesEnabled === true || particlesEnabled === false) {
|
||
return particlesEnabled
|
||
}
|
||
// If room override is null, use theme setting
|
||
return currentThemeData?.settings?.particlesEnabled ?? true
|
||
}
|
||
|
||
const actualParticlesEnabled = getActualParticlesEnabled()
|
||
const hasRoomOverride = particlesEnabled !== null && particlesEnabled !== undefined
|
||
|
||
// 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('')
|
||
const [answers, setAnswers] = useState([
|
||
{ text: '', points: 100 },
|
||
{ text: '', points: 80 },
|
||
{ text: '', points: 60 },
|
||
{ text: '', points: 40 },
|
||
{ text: '', points: 20 },
|
||
{ text: '', points: 10 },
|
||
])
|
||
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 [searchQuery, setSearchQuery] = useState('')
|
||
const [viewingQuestion, setViewingQuestion] = useState(null)
|
||
const [showAnswers, setShowAnswers] = useState(false)
|
||
|
||
// Сбрасываем вкладку на initialTab при открытии модального окна
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
setActiveTab(initialTab)
|
||
}
|
||
}, [isOpen, initialTab])
|
||
|
||
if (!isOpen) return null
|
||
|
||
const gameStatus = room?.status || 'WAITING'
|
||
const areAllAnswersRevealed = currentQuestion
|
||
? revealedAnswers.length === currentQuestion.answers.length
|
||
: false
|
||
|
||
// Handlers
|
||
const handleBackdropClick = (e) => {
|
||
if (e.target === e.currentTarget) onClose()
|
||
}
|
||
|
||
const handleRevealAnswer = (answerId) => {
|
||
if (revealedAnswers.includes(answerId)) {
|
||
onHideAnswer(answerId)
|
||
} else {
|
||
onRevealAnswer(answerId)
|
||
}
|
||
}
|
||
|
||
const handleAwardPoints = (points) => {
|
||
if (selectedPlayer) {
|
||
onAwardPoints(selectedPlayer, points)
|
||
}
|
||
}
|
||
|
||
const handlePenalty = () => {
|
||
if (selectedPlayer) {
|
||
onPenalty(selectedPlayer)
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
setQuestionText('')
|
||
setAnswers([
|
||
{ text: '', points: 100 },
|
||
{ text: '', points: 80 },
|
||
{ text: '', points: 60 },
|
||
{ text: '', points: 40 },
|
||
{ text: '', points: 20 },
|
||
{ text: '', points: 10 },
|
||
])
|
||
setJsonError('')
|
||
}
|
||
|
||
const handleEditQuestion = (question) => {
|
||
setEditingQuestion(question)
|
||
setQuestionText(question.text)
|
||
setAnswers([...question.answers])
|
||
setJsonError('')
|
||
}
|
||
|
||
const handleCancelEditQuestion = () => {
|
||
resetQuestionForm()
|
||
}
|
||
|
||
const handleAnswerChange = (index, field, value) => {
|
||
const updatedAnswers = [...answers]
|
||
if (field === 'text') {
|
||
updatedAnswers[index].text = value
|
||
} else if (field === 'points') {
|
||
updatedAnswers[index].points = parseInt(value) || 0
|
||
}
|
||
setAnswers(updatedAnswers)
|
||
}
|
||
|
||
const handleAddAnswer = () => {
|
||
const minPoints = Math.min(...answers.map(a => a.points))
|
||
setAnswers([...answers, { text: '', points: Math.max(0, minPoints - 10) }])
|
||
}
|
||
|
||
const handleRemoveAnswer = (index) => {
|
||
if (answers.length > 1) {
|
||
setAnswers(answers.filter((_, i) => i !== index))
|
||
}
|
||
}
|
||
|
||
const validateQuestionForm = () => {
|
||
if (!questionText.trim()) {
|
||
setJsonError('Введите текст вопроса')
|
||
return false
|
||
}
|
||
if (answers.length === 0) {
|
||
setJsonError('Добавьте хотя бы один ответ')
|
||
return false
|
||
}
|
||
const hasEmptyAnswers = answers.some(a => !a.text.trim())
|
||
if (hasEmptyAnswers) {
|
||
setJsonError('Заполните все ответы')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
const handleSaveQuestion = () => {
|
||
if (!validateQuestionForm()) return
|
||
|
||
const questionData = {
|
||
id: editingQuestion ? editingQuestion.id : Date.now(),
|
||
text: questionText.trim(),
|
||
answers: answers
|
||
.filter(a => a.text.trim())
|
||
.map(a => ({
|
||
text: a.text.trim(),
|
||
points: a.points,
|
||
})),
|
||
}
|
||
|
||
let updatedQuestions
|
||
if (editingQuestion) {
|
||
updatedQuestions = questions.map(q =>
|
||
q.id === editingQuestion.id ? questionData : q
|
||
)
|
||
} else {
|
||
updatedQuestions = [...questions, questionData]
|
||
}
|
||
|
||
onUpdateQuestions(updatedQuestions)
|
||
resetQuestionForm()
|
||
}
|
||
|
||
const handleDeleteQuestion = (questionId) => {
|
||
if (window.confirm('Вы уверены, что хотите удалить этот вопрос?')) {
|
||
const updatedQuestions = questions.filter(q => q.id !== questionId)
|
||
onUpdateQuestions(updatedQuestions)
|
||
if (editingQuestion && editingQuestion.id === questionId) {
|
||
resetQuestionForm()
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleExportJson = () => {
|
||
try {
|
||
const jsonString = JSON.stringify(questions, null, 2)
|
||
const blob = new Blob([jsonString], { type: 'application/json' })
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = 'questions.json'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
URL.revokeObjectURL(url)
|
||
setJsonError('')
|
||
} catch (error) {
|
||
setJsonError('Ошибка при экспорте: ' + error.message)
|
||
}
|
||
}
|
||
|
||
const handleDownloadTemplate = () => {
|
||
const template = [
|
||
{
|
||
text: 'Назовите самый популярный вид спорта в мире',
|
||
answers: [
|
||
{ text: 'Футбол', points: 100 },
|
||
{ text: 'Баскетбол', points: 80 },
|
||
{ text: 'Теннис', points: 60 },
|
||
{ text: 'Хоккей', points: 40 },
|
||
{ text: 'Волейбол', points: 20 },
|
||
{ text: 'Бокс', points: 10 },
|
||
]
|
||
},
|
||
{
|
||
text: 'Что люди обычно берут с собой на пляж?',
|
||
answers: [
|
||
{ text: 'Полотенце', points: 100 },
|
||
{ text: 'Крем от солнца', points: 80 },
|
||
{ text: 'Очки', points: 60 },
|
||
{ text: 'Зонт', points: 40 },
|
||
{ text: 'Книга', points: 20 },
|
||
{ text: 'Еда', points: 10 },
|
||
]
|
||
}
|
||
]
|
||
|
||
try {
|
||
const jsonString = JSON.stringify(template, null, 2)
|
||
const blob = new Blob([jsonString], { type: 'application/json' })
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = 'template_questions.json'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
URL.revokeObjectURL(url)
|
||
setJsonError('')
|
||
} catch (error) {
|
||
setJsonError('Ошибка при скачивании шаблона: ' + error.message)
|
||
}
|
||
}
|
||
|
||
const handleImportJson = () => {
|
||
const input = document.createElement('input')
|
||
input.type = 'file'
|
||
input.accept = '.json'
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0]
|
||
if (!file) return
|
||
|
||
const reader = new FileReader()
|
||
reader.onload = (event) => {
|
||
try {
|
||
const jsonContent = JSON.parse(event.target.result)
|
||
|
||
if (!Array.isArray(jsonContent)) {
|
||
setJsonError('JSON должен содержать массив вопросов')
|
||
return
|
||
}
|
||
|
||
// Валидация - id опционален (будет сгенерирован автоматически)
|
||
const isValid = jsonContent.every(q =>
|
||
typeof q.text === 'string' &&
|
||
Array.isArray(q.answers) &&
|
||
q.answers.every(a => a.text && typeof a.points === 'number')
|
||
)
|
||
|
||
if (!isValid) {
|
||
setJsonError('Неверный формат JSON. Ожидается массив объектов с полями: text, answers')
|
||
return
|
||
}
|
||
|
||
// Добавляем id если его нет
|
||
const questionsWithIds = jsonContent.map((q, idx) => ({
|
||
...q,
|
||
id: q.id || Date.now() + Math.random() + idx,
|
||
answers: q.answers.map((a, aidx) => ({
|
||
...a,
|
||
id: a.id || `answer-${Date.now()}-${idx}-${aidx}`
|
||
}))
|
||
}))
|
||
|
||
onUpdateQuestions(questionsWithIds)
|
||
setJsonError('')
|
||
alert(`Успешно импортировано ${questionsWithIds.length} вопросов`)
|
||
} catch (error) {
|
||
setJsonError('Ошибка при импорте: ' + error.message)
|
||
}
|
||
}
|
||
reader.readAsText(file)
|
||
}
|
||
input.click()
|
||
}
|
||
|
||
const handleSelectPack = async (packId) => {
|
||
if (!packId) {
|
||
setPackQuestions([])
|
||
setSelectedPack(null)
|
||
setSearchQuery('')
|
||
setViewingQuestion(null)
|
||
setShowAnswers(false)
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await questionsApi.getPack(packId)
|
||
setPackQuestions(response.data.questions || [])
|
||
setSelectedPack(packId)
|
||
setSelectedQuestionIndices(new Set())
|
||
setSearchQuery('')
|
||
setViewingQuestion(null)
|
||
setShowAnswers(false)
|
||
} catch (error) {
|
||
console.error('Error fetching pack:', error)
|
||
setJsonError('Ошибка загрузки пака вопросов')
|
||
}
|
||
}
|
||
|
||
// Фильтрация вопросов по поисковому запросу
|
||
const filteredPackQuestions = packQuestions.filter((q) => {
|
||
if (!searchQuery.trim()) return true
|
||
const questionText = (q.text || q.question || '').toLowerCase()
|
||
return questionText.includes(searchQuery.toLowerCase())
|
||
})
|
||
|
||
// Выбор всех видимых вопросов
|
||
const handleSelectAll = () => {
|
||
const allVisibleIndices = new Set(
|
||
filteredPackQuestions.map((q) => {
|
||
const originalIndex = packQuestions.findIndex(pq => pq === q)
|
||
return originalIndex
|
||
}).filter(idx => idx !== -1)
|
||
)
|
||
const newSelected = new Set(selectedQuestionIndices)
|
||
allVisibleIndices.forEach(idx => newSelected.add(idx))
|
||
setSelectedQuestionIndices(newSelected)
|
||
}
|
||
|
||
// Снятие выбора со всех видимых вопросов
|
||
const handleDeselectAll = () => {
|
||
const visibleIndices = new Set(
|
||
filteredPackQuestions.map((q) => {
|
||
const originalIndex = packQuestions.findIndex(pq => pq === q)
|
||
return originalIndex
|
||
}).filter(idx => idx !== -1)
|
||
)
|
||
const newSelected = new Set(selectedQuestionIndices)
|
||
visibleIndices.forEach(idx => newSelected.delete(idx))
|
||
setSelectedQuestionIndices(newSelected)
|
||
}
|
||
|
||
// Проверка, выбраны ли все видимые вопросы
|
||
const areAllVisibleSelected = () => {
|
||
if (filteredPackQuestions.length === 0) return false
|
||
const visibleIndices = filteredPackQuestions.map((q) => {
|
||
const originalIndex = packQuestions.findIndex(pq => pq === q)
|
||
return originalIndex
|
||
}).filter(idx => idx !== -1)
|
||
return visibleIndices.every(idx => selectedQuestionIndices.has(idx))
|
||
}
|
||
|
||
// Просмотр вопроса
|
||
const handleViewQuestion = (question) => {
|
||
setViewingQuestion(question)
|
||
setShowAnswers(false)
|
||
}
|
||
|
||
// Закрытие просмотра вопроса
|
||
const handleCloseViewer = () => {
|
||
setViewingQuestion(null)
|
||
setShowAnswers(false)
|
||
}
|
||
|
||
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)
|
||
|
||
const copiedQuestions = questionsToImport.map((q, idx) => ({
|
||
id: Date.now() + Math.random() + idx, // Generate new ID
|
||
text: q.text || q.question || '',
|
||
answers: (q.answers || []).map(a => ({ text: a.text, points: a.points })),
|
||
}))
|
||
|
||
const updatedQuestions = [...questions, ...copiedQuestions]
|
||
onUpdateQuestions(updatedQuestions)
|
||
|
||
setSelectedQuestionIndices(new Set())
|
||
setSearchQuery('')
|
||
setShowPackImport(false)
|
||
setJsonError('')
|
||
alert(`Импортировано ${copiedQuestions.length} вопросов`)
|
||
}
|
||
|
||
return (
|
||
<div className="game-mgmt-modal-backdrop" onClick={handleBackdropClick}>
|
||
<div className="game-mgmt-modal-content">
|
||
{/* Header with title and close button */}
|
||
<div className="game-mgmt-modal-header">
|
||
<h2>🎛 Управление игрой</h2>
|
||
<button className="game-mgmt-close" onClick={onClose}>×</button>
|
||
</div>
|
||
|
||
{/* Tabs navigation */}
|
||
<div className="game-mgmt-tabs">
|
||
<button
|
||
className={`tab ${activeTab === 'players' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('players')}
|
||
>
|
||
👥 Игроки
|
||
</button>
|
||
<button
|
||
className={`tab ${activeTab === 'game' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('game')}
|
||
>
|
||
🎮 Игра
|
||
</button>
|
||
<button
|
||
className={`tab ${activeTab === 'scoring' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('scoring')}
|
||
disabled={gameStatus !== 'PLAYING' || participants.length === 0}
|
||
>
|
||
➕ Очки
|
||
</button>
|
||
<button
|
||
className={`tab ${activeTab === 'questions' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('questions')}
|
||
>
|
||
❓ Вопросы
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tab content */}
|
||
<div className="game-mgmt-body">
|
||
{/* PLAYERS TAB */}
|
||
{activeTab === 'players' && (
|
||
<div className="tab-content">
|
||
<h3>Участники ({participants.length})</h3>
|
||
<div className="players-list">
|
||
{participants.length === 0 ? (
|
||
<p className="empty-message">Нет участников</p>
|
||
) : (
|
||
participants.map((participant) => (
|
||
<div key={participant.id} className="player-item">
|
||
<div className="player-info">
|
||
{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>
|
||
)}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
{onChangeParticipantRole ? (
|
||
<select
|
||
value={participant.role}
|
||
onChange={(e) => {
|
||
const newRole = e.target.value;
|
||
// Проверка: нельзя изменить роль последнего хоста на не-HOST
|
||
if (participant.role === 'HOST' && newRole !== 'HOST') {
|
||
const hostCount = participants.filter(p =>
|
||
p.role === 'HOST' && (p.isActive !== false)
|
||
).length;
|
||
if (hostCount <= 1) {
|
||
alert('Нельзя изменить роль последнего хоста');
|
||
return;
|
||
}
|
||
}
|
||
onChangeParticipantRole(participant.id, newRole);
|
||
}}
|
||
style={{
|
||
padding: '5px 10px',
|
||
background: 'rgba(255, 255, 255, 0.1)',
|
||
border: '1px solid rgba(255, 215, 0, 0.3)',
|
||
borderRadius: '6px',
|
||
color: 'var(--text-primary)',
|
||
fontSize: '0.9rem',
|
||
cursor: 'pointer',
|
||
}}
|
||
title="Изменить роль участника"
|
||
>
|
||
<option value="HOST">👑 Ведущий</option>
|
||
<option value="PLAYER">🎮 Игрок</option>
|
||
<option value="SPECTATOR">👀 Зритель</option>
|
||
</select>
|
||
) : (
|
||
<span className="player-role">
|
||
{participant.role === 'HOST' && '👑 Ведущий'}
|
||
{participant.role === 'SPECTATOR' && '👀 Зритель'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<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>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* GAME CONTROLS TAB */}
|
||
{activeTab === 'game' && (
|
||
<div className="tab-content">
|
||
<h3>Управление игрой</h3>
|
||
|
||
<div className="game-status">
|
||
<strong>Статус:</strong>
|
||
{gameStatus === 'WAITING' && ' Ожидание'}
|
||
{gameStatus === 'PLAYING' && ' Идет игра'}
|
||
{gameStatus === 'FINISHED' && ' Завершена'}
|
||
</div>
|
||
|
||
{gameStatus === 'WAITING' && (
|
||
<button
|
||
className="mgmt-button start-button"
|
||
onClick={onStartGame}
|
||
disabled={questions.length === 0}
|
||
>
|
||
▶️ Начать игру
|
||
</button>
|
||
)}
|
||
|
||
{gameStatus === 'PLAYING' && (
|
||
<div className="game-controls">
|
||
<div className="question-nav">
|
||
<button
|
||
className="mgmt-button"
|
||
onClick={onPreviousQuestion}
|
||
disabled={currentQuestionIndex === 0}
|
||
>
|
||
⏮ Предыдущий
|
||
</button>
|
||
<span className="question-indicator">
|
||
Вопрос {currentQuestionIndex + 1} / {totalQuestions}
|
||
</span>
|
||
<button
|
||
className="mgmt-button"
|
||
onClick={onNextQuestion}
|
||
disabled={currentQuestionIndex >= totalQuestions - 1}
|
||
>
|
||
Следующий ⏭
|
||
</button>
|
||
</div>
|
||
<button
|
||
className="mgmt-button end-button"
|
||
onClick={onEndGame}
|
||
>
|
||
⏹ Завершить игру
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Управление ответами - показывается только во время активной игры */}
|
||
{gameStatus === 'PLAYING' && currentQuestion && (
|
||
<div className="answers-control-section">
|
||
<h3>Управление ответами</h3>
|
||
|
||
<button
|
||
className="mgmt-button toggle-all-button"
|
||
onClick={areAllAnswersRevealed ? onHideAllAnswers : onShowAllAnswers}
|
||
>
|
||
{areAllAnswersRevealed ? '🙈 Скрыть все' : '👁 Показать все'}
|
||
</button>
|
||
|
||
<div className="answers-grid">
|
||
{currentQuestion.answers.map((answer, index) => (
|
||
<button
|
||
key={answer.id || index}
|
||
className={`answer-button ${
|
||
revealedAnswers.includes(answer.id) ? 'revealed' : 'hidden'
|
||
}`}
|
||
onClick={() => handleRevealAnswer(answer.id)}
|
||
>
|
||
<span className="answer-num">{index + 1}</span>
|
||
<span className="answer-txt">{answer.text}</span>
|
||
<span className="answer-pts">{answer.points}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="game-info">
|
||
<div className="info-item">
|
||
<span>Игроков:</span> <strong>{participants.length}</strong>
|
||
</div>
|
||
{gameStatus === 'PLAYING' && totalQuestions > 0 && (
|
||
<div className="info-item">
|
||
<span>Вопросов:</span> <strong>{totalQuestions}</strong>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Visual Effects Section */}
|
||
{onToggleParticles && (
|
||
<div className="visual-effects-section">
|
||
<h3>🎨 Визуальные эффекты</h3>
|
||
<div className="visual-effects-controls">
|
||
<label className="toggle-label">
|
||
<input
|
||
type="checkbox"
|
||
checked={actualParticlesEnabled}
|
||
onChange={(e) => {
|
||
if (onToggleParticles) {
|
||
onToggleParticles(e.target.checked)
|
||
}
|
||
}}
|
||
className="toggle-checkbox"
|
||
/>
|
||
<span className="toggle-text">
|
||
{hasRoomOverride
|
||
? (particlesEnabled ? 'Частицы включены (переопределено)' : 'Частицы выключены (переопределено)')
|
||
: `Частицы ${actualParticlesEnabled ? 'включены' : 'выключены'} (по умолчанию из темы)`
|
||
}
|
||
</span>
|
||
</label>
|
||
<p className="visual-effects-description">
|
||
{hasRoomOverride
|
||
? 'Вы переопределили настройку из темы. Переключение изменит настройку комнаты.'
|
||
: 'Текущее состояние берется из настроек темы. Переключение создаст переопределение для этой комнаты.'
|
||
}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* SCORING TAB */}
|
||
{activeTab === 'scoring' && (
|
||
<div className="tab-content">
|
||
<h3>Начисление очков</h3>
|
||
|
||
<div className="player-selector">
|
||
<label>Выберите игрока:</label>
|
||
<select
|
||
value={selectedPlayer || ''}
|
||
onChange={(e) => setSelectedPlayer(e.target.value || null)}
|
||
>
|
||
<option value="">-- Выберите игрока --</option>
|
||
{participants
|
||
.filter((p) => p.role === 'PLAYER')
|
||
.map((player) => (
|
||
<option key={player.id} value={player.id}>
|
||
{player.name} ({player.score || 0} очков)
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{selectedPlayer && (
|
||
<div className="scoring-section">
|
||
<div className="quick-points">
|
||
<button
|
||
className="mgmt-button points-button"
|
||
onClick={() => handleAwardPoints(5)}
|
||
>
|
||
+5
|
||
</button>
|
||
<button
|
||
className="mgmt-button points-button"
|
||
onClick={() => handleAwardPoints(10)}
|
||
>
|
||
+10
|
||
</button>
|
||
<button
|
||
className="mgmt-button points-button"
|
||
onClick={() => handleAwardPoints(20)}
|
||
>
|
||
+20
|
||
</button>
|
||
<button
|
||
className="mgmt-button penalty-button"
|
||
onClick={handlePenalty}
|
||
>
|
||
❌ Промах
|
||
</button>
|
||
</div>
|
||
|
||
<div className="custom-points">
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
max="100"
|
||
value={customPoints}
|
||
onChange={(e) => setCustomPoints(parseInt(e.target.value) || 0)}
|
||
/>
|
||
<button
|
||
className="mgmt-button custom-button"
|
||
onClick={() => handleAwardPoints(customPoints)}
|
||
>
|
||
Начислить {customPoints}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* QUESTIONS TAB */}
|
||
{activeTab === 'questions' && (
|
||
<div className="tab-content questions-tab-content">
|
||
<h3>Управление вопросами</h3>
|
||
|
||
<div className="questions-modal-actions">
|
||
<button
|
||
className="questions-modal-template-button"
|
||
onClick={handleDownloadTemplate}
|
||
>
|
||
📋 Скачать шаблон
|
||
</button>
|
||
<button
|
||
className="questions-modal-import-button"
|
||
onClick={handleImportJson}
|
||
>
|
||
📤 Импорт JSON
|
||
</button>
|
||
<button
|
||
className="questions-modal-export-button"
|
||
onClick={handleExportJson}
|
||
>
|
||
📥 Экспорт JSON
|
||
</button>
|
||
{availablePacks.length > 0 && (
|
||
<button
|
||
className="questions-modal-pack-import-button"
|
||
onClick={() => setShowPackImport(!showPackImport)}
|
||
>
|
||
📦 {showPackImport ? 'Скрыть импорт' : 'Импорт из пака'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{jsonError && (
|
||
<div className="questions-modal-error">{jsonError}</div>
|
||
)}
|
||
|
||
{showPackImport && availablePacks.length > 0 && (
|
||
<div className="pack-import-section">
|
||
<h4>Импорт вопросов из пака</h4>
|
||
<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-search-container">
|
||
<input
|
||
type="text"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
placeholder="🔍 Поиск вопросов..."
|
||
className="pack-search-input"
|
||
/>
|
||
</div>
|
||
|
||
<div className="pack-questions-header">
|
||
<div className="pack-questions-header-left">
|
||
<span>Выберите вопросы для импорта:</span>
|
||
<div className="pack-select-all-buttons">
|
||
{filteredPackQuestions.length > 0 && (
|
||
<>
|
||
{areAllVisibleSelected() ? (
|
||
<button
|
||
onClick={handleDeselectAll}
|
||
className="pack-select-all-button"
|
||
>
|
||
Снять выбор
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={handleSelectAll}
|
||
className="pack-select-all-button"
|
||
>
|
||
Выбрать все ({filteredPackQuestions.length})
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={handleImportSelected}
|
||
disabled={selectedQuestionIndices.size === 0}
|
||
className="pack-import-confirm-button"
|
||
>
|
||
Импортировать ({selectedQuestionIndices.size})
|
||
</button>
|
||
</div>
|
||
|
||
<div className="pack-questions-items">
|
||
{filteredPackQuestions.length === 0 ? (
|
||
<div className="pack-no-results">
|
||
{searchQuery ? 'Вопросы не найдены' : 'Нет вопросов в паке'}
|
||
</div>
|
||
) : (
|
||
filteredPackQuestions.map((q, filteredIdx) => {
|
||
const originalIndex = packQuestions.findIndex(pq => pq === q)
|
||
return (
|
||
<div key={originalIndex} className="pack-question-item">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedQuestionIndices.has(originalIndex)}
|
||
onChange={() => handleToggleQuestion(originalIndex)}
|
||
/>
|
||
<div className="pack-question-content">
|
||
<strong>{q.text || q.question}</strong>
|
||
<span className="pack-question-info">
|
||
{q.answers?.length || 0} ответов
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={() => handleViewQuestion(q)}
|
||
className="pack-view-question-button"
|
||
title="Просмотр вопроса"
|
||
>
|
||
👁
|
||
</button>
|
||
</div>
|
||
)
|
||
})
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Модальное окно просмотра вопроса */}
|
||
{viewingQuestion && (
|
||
<div className="pack-question-viewer-backdrop" onClick={handleCloseViewer}>
|
||
<div className="pack-question-viewer" onClick={(e) => e.stopPropagation()}>
|
||
<div className="pack-question-viewer-header">
|
||
<h4>Просмотр вопроса</h4>
|
||
<button
|
||
className="pack-question-viewer-close"
|
||
onClick={handleCloseViewer}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="pack-question-viewer-content">
|
||
<div className="pack-question-viewer-text">
|
||
{viewingQuestion.text || viewingQuestion.question}
|
||
</div>
|
||
<button
|
||
className="pack-show-answers-button"
|
||
onClick={() => setShowAnswers(!showAnswers)}
|
||
>
|
||
{showAnswers ? '🙈 Скрыть ответы' : '👁 Показать ответы'}
|
||
</button>
|
||
{showAnswers && (
|
||
<div className="pack-question-answers">
|
||
{viewingQuestion.answers?.map((answer, idx) => (
|
||
<div key={idx} className="pack-answer-item">
|
||
<span className="pack-answer-text">{answer.text}</span>
|
||
<span className="pack-answer-points">{answer.points} очков</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="questions-modal-form">
|
||
<input
|
||
type="text"
|
||
value={questionText}
|
||
onChange={(e) => setQuestionText(e.target.value)}
|
||
placeholder="Введите текст вопроса"
|
||
className="questions-modal-input"
|
||
/>
|
||
|
||
<div className="questions-modal-answers">
|
||
<div className="questions-modal-answers-header">
|
||
<span>Ответы:</span>
|
||
<button
|
||
className="questions-modal-add-answer-button"
|
||
onClick={handleAddAnswer}
|
||
type="button"
|
||
>
|
||
+ Добавить ответ
|
||
</button>
|
||
</div>
|
||
|
||
{answers.map((answer, index) => (
|
||
<div key={index} className="questions-modal-answer-row">
|
||
<input
|
||
type="text"
|
||
value={answer.text}
|
||
onChange={(e) => handleAnswerChange(index, 'text', e.target.value)}
|
||
placeholder={`Ответ ${index + 1}`}
|
||
className="questions-modal-answer-input"
|
||
/>
|
||
<input
|
||
type="number"
|
||
value={answer.points}
|
||
onChange={(e) => handleAnswerChange(index, 'points', e.target.value)}
|
||
className="questions-modal-points-input"
|
||
min="0"
|
||
/>
|
||
{answers.length > 1 && (
|
||
<button
|
||
className="questions-modal-remove-answer-button"
|
||
onClick={() => handleRemoveAnswer(index)}
|
||
type="button"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="questions-modal-form-buttons">
|
||
<button
|
||
className="questions-modal-save-button"
|
||
onClick={handleSaveQuestion}
|
||
>
|
||
{editingQuestion ? 'Сохранить изменения' : 'Добавить вопрос'}
|
||
</button>
|
||
{editingQuestion && (
|
||
<button
|
||
className="questions-modal-cancel-button"
|
||
onClick={handleCancelEditQuestion}
|
||
>
|
||
Отмена
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="questions-modal-list">
|
||
<h4 className="questions-modal-list-title">
|
||
Вопросы ({questions.length})
|
||
</h4>
|
||
{questions.length === 0 ? (
|
||
<p className="questions-modal-empty">Нет вопросов. Добавьте вопросы для игры.</p>
|
||
) : (
|
||
<div className="questions-modal-items">
|
||
{questions.map((question) => (
|
||
<div key={question.id} className="questions-modal-item">
|
||
<div className="questions-modal-item-content">
|
||
<div className="questions-modal-item-text">{question.text}</div>
|
||
<div className="questions-modal-item-info">
|
||
{question.answers.length} ответов
|
||
</div>
|
||
</div>
|
||
<div className="questions-modal-item-actions">
|
||
<button
|
||
className="questions-modal-edit-button"
|
||
onClick={() => handleEditQuestion(question)}
|
||
title="Редактировать"
|
||
>
|
||
✏️
|
||
</button>
|
||
<button
|
||
className="questions-modal-delete-button"
|
||
onClick={() => handleDeleteQuestion(question.id)}
|
||
title="Удалить"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default GameManagementModal
|
||
|