This commit is contained in:
Dmitry 2026-01-09 00:19:49 +03:00
parent a02a0d5d1d
commit 0c86868e0f
3 changed files with 601 additions and 38 deletions

View file

@ -46,6 +46,9 @@ const GameManagementModal = ({
const [selectedPack, setSelectedPack] = useState(null) const [selectedPack, setSelectedPack] = useState(null)
const [packQuestions, setPackQuestions] = useState([]) const [packQuestions, setPackQuestions] = useState([])
const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set()) const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set())
const [searchQuery, setSearchQuery] = useState('')
const [viewingQuestion, setViewingQuestion] = useState(null)
const [showAnswers, setShowAnswers] = useState(false)
if (!isOpen) return null if (!isOpen) return null
@ -244,6 +247,9 @@ const GameManagementModal = ({
if (!packId) { if (!packId) {
setPackQuestions([]) setPackQuestions([])
setSelectedPack(null) setSelectedPack(null)
setSearchQuery('')
setViewingQuestion(null)
setShowAnswers(false)
return return
} }
@ -252,12 +258,70 @@ const GameManagementModal = ({
setPackQuestions(response.data.questions || []) setPackQuestions(response.data.questions || [])
setSelectedPack(packId) setSelectedPack(packId)
setSelectedQuestionIndices(new Set()) setSelectedQuestionIndices(new Set())
setSearchQuery('')
setViewingQuestion(null)
setShowAnswers(false)
} catch (error) { } catch (error) {
console.error('Error fetching pack:', error) console.error('Error fetching pack:', error)
setJsonError('Ошибка загрузки пака вопросов') 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 handleToggleQuestion = (index) => {
const newSelected = new Set(selectedQuestionIndices) const newSelected = new Set(selectedQuestionIndices)
if (newSelected.has(index)) { if (newSelected.has(index)) {
@ -272,16 +336,17 @@ const GameManagementModal = ({
const indices = Array.from(selectedQuestionIndices) const indices = Array.from(selectedQuestionIndices)
const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean) const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean)
const copiedQuestions = questionsToImport.map(q => ({ const copiedQuestions = questionsToImport.map((q, idx) => ({
id: Date.now() + Math.random(), id: Date.now() + Math.random() + idx, // Generate new ID
text: q.text, text: q.text || q.question || '',
answers: q.answers.map(a => ({ text: a.text, points: a.points })), answers: (q.answers || []).map(a => ({ text: a.text, points: a.points })),
})) }))
const updatedQuestions = [...questions, ...copiedQuestions] const updatedQuestions = [...questions, ...copiedQuestions]
onUpdateQuestions(updatedQuestions) onUpdateQuestions(updatedQuestions)
setSelectedQuestionIndices(new Set()) setSelectedQuestionIndices(new Set())
setSearchQuery('')
setShowPackImport(false) setShowPackImport(false)
setJsonError('') setJsonError('')
alert(`Импортировано ${copiedQuestions.length} вопросов`) alert(`Импортировано ${copiedQuestions.length} вопросов`)
@ -577,8 +642,42 @@ const GameManagementModal = ({
{packQuestions.length > 0 && ( {packQuestions.length > 0 && (
<div className="pack-questions-list"> <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">
<div className="pack-questions-header-left">
<span>Выберите вопросы для импорта:</span> <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 <button
onClick={handleImportSelected} onClick={handleImportSelected}
disabled={selectedQuestionIndices.size === 0} disabled={selectedQuestionIndices.size === 0}
@ -589,22 +688,76 @@ const GameManagementModal = ({
</div> </div>
<div className="pack-questions-items"> <div className="pack-questions-items">
{packQuestions.map((q, idx) => ( {filteredPackQuestions.length === 0 ? (
<div key={idx} className="pack-question-item"> <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 <input
type="checkbox" type="checkbox"
checked={selectedQuestionIndices.has(idx)} checked={selectedQuestionIndices.has(originalIndex)}
onChange={() => handleToggleQuestion(idx)} onChange={() => handleToggleQuestion(originalIndex)}
/> />
<div className="pack-question-content"> <div className="pack-question-content">
<strong>{q.text}</strong> <strong>{q.text || q.question}</strong>
<span className="pack-question-info"> <span className="pack-question-info">
{q.answers.length} ответов {q.answers?.length || 0} ответов
</span> </span>
</div> </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> </div>
)} )}
</div> </div>

View file

@ -582,6 +582,234 @@
font-size: 0.85rem; font-size: 0.85rem;
} }
/* Search */
.pack-search-container {
margin-bottom: 15px;
}
.pack-search-input {
width: 100%;
padding: 12px 15px;
border: 2px solid rgba(255, 215, 0, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
}
.pack-search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.pack-search-input:focus {
border-color: #ffd700;
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
}
/* Header improvements */
.pack-questions-header-left {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
.pack-select-all-buttons {
display: flex;
gap: 10px;
}
.pack-select-all-button {
padding: 8px 15px;
background: rgba(78, 205, 196, 0.3);
color: #4ecdc4;
border: 2px solid rgba(78, 205, 196, 0.5);
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.pack-select-all-button:hover {
background: rgba(78, 205, 196, 0.5);
border-color: #4ecdc4;
transform: translateY(-2px);
}
/* View question button */
.pack-view-question-button {
padding: 8px 12px;
background: rgba(102, 126, 234, 0.3);
color: #667eea;
border: 2px solid rgba(102, 126, 234, 0.5);
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
margin-left: 10px;
}
.pack-view-question-button:hover {
background: rgba(102, 126, 234, 0.5);
border-color: #667eea;
transform: scale(1.1);
}
/* No results */
.pack-no-results {
text-align: center;
color: rgba(255, 255, 255, 0.6);
padding: 40px 20px;
font-size: 1.1rem;
}
/* Question viewer modal */
.pack-question-viewer-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
padding: 20px;
}
.pack-question-viewer {
background: rgba(20, 20, 30, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 25px;
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
border: 2px solid rgba(255, 215, 0, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
position: relative;
}
.pack-question-viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255, 215, 0, 0.2);
}
.pack-question-viewer-header h4 {
color: #ffd700;
font-size: 1.5rem;
margin: 0;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
.pack-question-viewer-close {
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
border: 2px solid rgba(255, 107, 107, 0.5);
border-radius: 50%;
width: 35px;
height: 35px;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.pack-question-viewer-close:hover {
background: rgba(255, 107, 107, 0.4);
border-color: #ff6b6b;
transform: scale(1.1);
}
.pack-question-viewer-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.pack-question-viewer-text {
color: #fff;
font-size: 1.2rem;
line-height: 1.6;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 2px solid rgba(255, 215, 0, 0.2);
}
.pack-show-answers-button {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
align-self: flex-start;
}
.pack-show-answers-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.pack-question-answers {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.pack-answer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 215, 0, 0.2);
border-radius: 12px;
transition: all 0.3s ease;
}
.pack-answer-item:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 215, 0, 0.4);
}
.pack-answer-text {
color: #fff;
font-size: 1rem;
flex: 1;
word-wrap: break-word;
margin-right: 15px;
}
.pack-answer-points {
color: #ffd700;
font-size: 1rem;
font-weight: bold;
flex-shrink: 0;
text-shadow: 0 0 5px rgba(255, 215, 0, 0.5);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.pack-import-section { .pack-import-section {
padding: 15px; padding: 15px;
@ -593,8 +821,37 @@
gap: 10px; gap: 10px;
} }
.pack-questions-header-left {
width: 100%;
}
.pack-import-confirm-button { .pack-import-confirm-button {
width: 100%; width: 100%;
} }
.pack-question-item {
flex-wrap: wrap;
}
.pack-view-question-button {
width: 100%;
margin-left: 0;
margin-top: 10px;
}
.pack-question-viewer {
padding: 20px;
max-height: 90vh;
}
.pack-answer-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.pack-answer-points {
align-self: flex-end;
}
} }

View file

@ -27,6 +27,9 @@ const QuestionsModal = ({
const [packQuestions, setPackQuestions] = useState([]) const [packQuestions, setPackQuestions] = useState([])
const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set()) const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set())
const [savingToRoom, setSavingToRoom] = useState(false) const [savingToRoom, setSavingToRoom] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [viewingQuestion, setViewingQuestion] = useState(null)
const [showAnswers, setShowAnswers] = useState(false)
if (!isOpen) return null if (!isOpen) return null
@ -207,6 +210,9 @@ const QuestionsModal = ({
if (!packId) { if (!packId) {
setPackQuestions([]) setPackQuestions([])
setSelectedPack(null) setSelectedPack(null)
setSearchQuery('')
setViewingQuestion(null)
setShowAnswers(false)
return return
} }
@ -215,12 +221,70 @@ const QuestionsModal = ({
setPackQuestions(response.data.questions || []) setPackQuestions(response.data.questions || [])
setSelectedPack(packId) setSelectedPack(packId)
setSelectedQuestionIndices(new Set()) setSelectedQuestionIndices(new Set())
setSearchQuery('')
setViewingQuestion(null)
setShowAnswers(false)
} catch (error) { } catch (error) {
console.error('Error fetching pack:', error) console.error('Error fetching pack:', error)
setJsonError('Ошибка загрузки пака вопросов') 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 handleToggleQuestion = (index) => {
const newSelected = new Set(selectedQuestionIndices) const newSelected = new Set(selectedQuestionIndices)
if (newSelected.has(index)) { if (newSelected.has(index)) {
@ -236,10 +300,10 @@ const QuestionsModal = ({
const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean) const questionsToImport = indices.map(idx => packQuestions[idx]).filter(Boolean)
// Create deep copies // Create deep copies
const copiedQuestions = questionsToImport.map(q => ({ const copiedQuestions = questionsToImport.map((q, idx) => ({
id: Date.now() + Math.random(), // Generate new ID id: Date.now() + Math.random() + idx, // Generate new ID
text: q.text, text: q.text || q.question || '',
answers: q.answers.map(a => ({ text: a.text, points: a.points })), answers: (q.answers || []).map(a => ({ text: a.text, points: a.points })),
})) }))
const updatedQuestions = [...questions, ...copiedQuestions] const updatedQuestions = [...questions, ...copiedQuestions]
@ -247,6 +311,7 @@ const QuestionsModal = ({
// Reset // Reset
setSelectedQuestionIndices(new Set()) setSelectedQuestionIndices(new Set())
setSearchQuery('')
setShowPackImport(false) setShowPackImport(false)
setJsonError('') setJsonError('')
alert(`Импортировано ${copiedQuestions.length} вопросов`) alert(`Импортировано ${copiedQuestions.length} вопросов`)
@ -307,8 +372,42 @@ const QuestionsModal = ({
{packQuestions.length > 0 && ( {packQuestions.length > 0 && (
<div className="pack-questions-list"> <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">
<div className="pack-questions-header-left">
<span>Выберите вопросы для импорта:</span> <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 <button
onClick={handleImportSelected} onClick={handleImportSelected}
disabled={selectedQuestionIndices.size === 0} disabled={selectedQuestionIndices.size === 0}
@ -319,22 +418,76 @@ const QuestionsModal = ({
</div> </div>
<div className="pack-questions-items"> <div className="pack-questions-items">
{packQuestions.map((q, idx) => ( {filteredPackQuestions.length === 0 ? (
<div key={idx} className="pack-question-item"> <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 <input
type="checkbox" type="checkbox"
checked={selectedQuestionIndices.has(idx)} checked={selectedQuestionIndices.has(originalIndex)}
onChange={() => handleToggleQuestion(idx)} onChange={() => handleToggleQuestion(originalIndex)}
/> />
<div className="pack-question-content"> <div className="pack-question-content">
<strong>{q.text}</strong> <strong>{q.text || q.question}</strong>
<span className="pack-question-info"> <span className="pack-question-info">
{q.answers.length} ответов {q.answers?.length || 0} ответов
</span> </span>
</div> </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> </div>
)} )}
</div> </div>