sto-k-odnomu/src/components/GameManagementModal.jsx
2026-01-10 19:16:44 +03:00

1144 lines
44 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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