2026-01-08 20:14:58 +00:00
|
|
|
|
import { useState } from 'react'
|
2026-01-08 20:59:44 +00:00
|
|
|
|
import { questionsApi } from '../services/api'
|
2026-01-08 20:14:58 +00:00
|
|
|
|
import './GameManagementModal.css'
|
2026-01-08 20:59:44 +00:00
|
|
|
|
import './QuestionsModal.css'
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
|
|
|
|
|
const GameManagementModal = ({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
room,
|
|
|
|
|
|
participants,
|
|
|
|
|
|
currentQuestion,
|
|
|
|
|
|
currentQuestionIndex,
|
|
|
|
|
|
totalQuestions,
|
|
|
|
|
|
revealedAnswers,
|
2026-01-08 20:59:44 +00:00
|
|
|
|
questions = [],
|
|
|
|
|
|
onUpdateQuestions,
|
|
|
|
|
|
availablePacks = [],
|
2026-01-08 20:14:58 +00:00
|
|
|
|
onStartGame,
|
|
|
|
|
|
onEndGame,
|
|
|
|
|
|
onNextQuestion,
|
|
|
|
|
|
onPreviousQuestion,
|
|
|
|
|
|
onRevealAnswer,
|
|
|
|
|
|
onHideAnswer,
|
|
|
|
|
|
onShowAllAnswers,
|
|
|
|
|
|
onHideAllAnswers,
|
|
|
|
|
|
onAwardPoints,
|
|
|
|
|
|
onPenalty,
|
2026-01-09 16:44:33 +00:00
|
|
|
|
onUpdatePlayerName,
|
|
|
|
|
|
onUpdatePlayerScore,
|
|
|
|
|
|
onKickPlayer,
|
2026-01-08 20:14:58 +00:00
|
|
|
|
}) => {
|
2026-01-10 00:18:08 +00:00
|
|
|
|
const [activeTab, setActiveTab] = useState('players') // players | game | scoring | questions
|
2026-01-08 20:14:58 +00:00
|
|
|
|
const [selectedPlayer, setSelectedPlayer] = useState(null)
|
|
|
|
|
|
const [customPoints, setCustomPoints] = useState(10)
|
2026-01-09 16:44:33 +00:00
|
|
|
|
|
|
|
|
|
|
// Player editing state
|
|
|
|
|
|
const [editingPlayerId, setEditingPlayerId] = useState(null)
|
|
|
|
|
|
const [editingPlayerName, setEditingPlayerName] = useState('')
|
|
|
|
|
|
const [editingPlayerScore, setEditingPlayerScore] = useState('')
|
|
|
|
|
|
const [editMode, setEditMode] = useState(null) // 'name' | 'score'
|
2026-01-08 20:59:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 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())
|
2026-01-08 21:19:49 +00:00
|
|
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
|
|
const [viewingQuestion, setViewingQuestion] = useState(null)
|
|
|
|
|
|
const [showAnswers, setShowAnswers] = useState(false)
|
2026-01-08 20:14:58 +00:00
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:44:38 +00:00
|
|
|
|
const handleRevealAnswer = (answerId) => {
|
|
|
|
|
|
if (revealedAnswers.includes(answerId)) {
|
|
|
|
|
|
onHideAnswer(answerId)
|
2026-01-08 20:14:58 +00:00
|
|
|
|
} else {
|
2026-01-08 21:44:38 +00:00
|
|
|
|
onRevealAnswer(answerId)
|
2026-01-08 20:14:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleAwardPoints = (points) => {
|
|
|
|
|
|
if (selectedPlayer) {
|
|
|
|
|
|
onAwardPoints(selectedPlayer, points)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePenalty = () => {
|
|
|
|
|
|
if (selectedPlayer) {
|
|
|
|
|
|
onPenalty(selectedPlayer)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 16:44:33 +00:00
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 20:59:44 +00:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 20:59:44 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
// Валидация - id опционален (будет сгенерирован автоматически)
|
2026-01-08 20:59:44 +00:00
|
|
|
|
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) {
|
2026-01-09 21:36:49 +00:00
|
|
|
|
setJsonError('Неверный формат JSON. Ожидается массив объектов с полями: text, answers')
|
2026-01-08 20:59:44 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:36:49 +00:00
|
|
|
|
// Добавляем 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)
|
2026-01-08 20:59:44 +00:00
|
|
|
|
setJsonError('')
|
2026-01-09 21:36:49 +00:00
|
|
|
|
alert(`Успешно импортировано ${questionsWithIds.length} вопросов`)
|
2026-01-08 20:59:44 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setJsonError('Ошибка при импорте: ' + error.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
reader.readAsText(file)
|
|
|
|
|
|
}
|
|
|
|
|
|
input.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectPack = async (packId) => {
|
|
|
|
|
|
if (!packId) {
|
|
|
|
|
|
setPackQuestions([])
|
|
|
|
|
|
setSelectedPack(null)
|
2026-01-08 21:19:49 +00:00
|
|
|
|
setSearchQuery('')
|
|
|
|
|
|
setViewingQuestion(null)
|
|
|
|
|
|
setShowAnswers(false)
|
2026-01-08 20:59:44 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await questionsApi.getPack(packId)
|
|
|
|
|
|
setPackQuestions(response.data.questions || [])
|
|
|
|
|
|
setSelectedPack(packId)
|
|
|
|
|
|
setSelectedQuestionIndices(new Set())
|
2026-01-08 21:19:49 +00:00
|
|
|
|
setSearchQuery('')
|
|
|
|
|
|
setViewingQuestion(null)
|
|
|
|
|
|
setShowAnswers(false)
|
2026-01-08 20:59:44 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error fetching pack:', error)
|
|
|
|
|
|
setJsonError('Ошибка загрузки пака вопросов')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 21:19:49 +00:00
|
|
|
|
// Фильтрация вопросов по поисковому запросу
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 20:59:44 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-01-08 21:19:49 +00:00
|
|
|
|
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 })),
|
2026-01-08 20:59:44 +00:00
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const updatedQuestions = [...questions, ...copiedQuestions]
|
|
|
|
|
|
onUpdateQuestions(updatedQuestions)
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedQuestionIndices(new Set())
|
2026-01-08 21:19:49 +00:00
|
|
|
|
setSearchQuery('')
|
2026-01-08 20:59:44 +00:00
|
|
|
|
setShowPackImport(false)
|
|
|
|
|
|
setJsonError('')
|
|
|
|
|
|
alert(`Импортировано ${copiedQuestions.length} вопросов`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 20:14:58 +00:00
|
|
|
|
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>
|
2026-01-08 20:59:44 +00:00
|
|
|
|
<button
|
|
|
|
|
|
className={`tab ${activeTab === 'questions' ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => setActiveTab('questions')}
|
|
|
|
|
|
>
|
|
|
|
|
|
❓ Вопросы
|
|
|
|
|
|
</button>
|
2026-01-08 20:14:58 +00:00
|
|
|
|
</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">
|
2026-01-09 16:44:33 +00:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
<span className="player-role">
|
|
|
|
|
|
{participant.role === 'HOST' && '👑 Ведущий'}
|
|
|
|
|
|
{participant.role === 'SPECTATOR' && '👀 Зритель'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-01-09 16:44:33 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
</div>
|
2026-01-09 16:44:33 +00:00
|
|
|
|
{participant.role !== 'HOST' && onKickPlayer && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="player-kick-btn"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (window.confirm(`Удалить игрока ${participant.name}?`)) {
|
|
|
|
|
|
onKickPlayer(participant.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
title="Удалить игрока"
|
|
|
|
|
|
>
|
|
|
|
|
|
🚫
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
</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={participants.length < 2}
|
|
|
|
|
|
>
|
|
|
|
|
|
▶️ Начать игру
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-01-10 00:18:08 +00:00
|
|
|
|
{/* Управление ответами - показывается только во время активной игры */}
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-01-08 20:14:58 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
2026-01-08 20:59:44 +00:00
|
|
|
|
|
|
|
|
|
|
{/* QUESTIONS TAB */}
|
|
|
|
|
|
{activeTab === 'questions' && (
|
|
|
|
|
|
<div className="tab-content questions-tab-content">
|
|
|
|
|
|
<h3>Управление вопросами</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="questions-modal-actions">
|
|
|
|
|
|
<button
|
2026-01-09 21:36:49 +00:00
|
|
|
|
className="questions-modal-template-button"
|
|
|
|
|
|
onClick={handleDownloadTemplate}
|
2026-01-08 20:59:44 +00:00
|
|
|
|
>
|
2026-01-09 21:36:49 +00:00
|
|
|
|
📋 Скачать шаблон
|
2026-01-08 20:59:44 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="questions-modal-import-button"
|
|
|
|
|
|
onClick={handleImportJson}
|
|
|
|
|
|
>
|
|
|
|
|
|
📤 Импорт JSON
|
|
|
|
|
|
</button>
|
2026-01-09 21:36:49 +00:00
|
|
|
|
<button
|
|
|
|
|
|
className="questions-modal-export-button"
|
|
|
|
|
|
onClick={handleExportJson}
|
|
|
|
|
|
>
|
|
|
|
|
|
📥 Экспорт JSON
|
|
|
|
|
|
</button>
|
2026-01-08 20:59:44 +00:00
|
|
|
|
{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">
|
2026-01-08 21:19:49 +00:00
|
|
|
|
{/* Поиск */}
|
|
|
|
|
|
<div className="pack-search-container">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
|
placeholder="🔍 Поиск вопросов..."
|
|
|
|
|
|
className="pack-search-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-08 20:59:44 +00:00
|
|
|
|
<div className="pack-questions-header">
|
2026-01-08 21:19:49 +00:00
|
|
|
|
<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>
|
2026-01-08 20:59:44 +00:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleImportSelected}
|
|
|
|
|
|
disabled={selectedQuestionIndices.size === 0}
|
|
|
|
|
|
className="pack-import-confirm-button"
|
|
|
|
|
|
>
|
|
|
|
|
|
Импортировать ({selectedQuestionIndices.size})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="pack-questions-items">
|
2026-01-08 21:19:49 +00:00
|
|
|
|
{filteredPackQuestions.length === 0 ? (
|
|
|
|
|
|
<div className="pack-no-results">
|
|
|
|
|
|
{searchQuery ? 'Вопросы не найдены' : 'Нет вопросов в паке'}
|
2026-01-08 20:59:44 +00:00
|
|
|
|
</div>
|
2026-01-08 21:19:49 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
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>
|
2026-01-08 20:59:44 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
2026-01-08 20:14:58 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default GameManagementModal
|
|
|
|
|
|
|