fixes
This commit is contained in:
parent
dbc43d65c1
commit
eb13424f20
20 changed files with 409 additions and 66 deletions
|
|
@ -211,7 +211,7 @@ export const packsApi = {
|
|||
getTemplate: async (): Promise<{
|
||||
templateVersion: string
|
||||
instructions: string
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
|
||||
}> => {
|
||||
try {
|
||||
const response = await adminApiClient.get('/api/admin/packs/export/template')
|
||||
|
|
@ -239,7 +239,7 @@ export const packsApi = {
|
|||
category: string
|
||||
isPublic: boolean
|
||||
}
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
|
||||
}> => {
|
||||
try {
|
||||
const response = await adminApiClient.get(`/api/admin/packs/${packId}/export`)
|
||||
|
|
@ -273,7 +273,7 @@ export const packsApi = {
|
|||
description: string
|
||||
category: string
|
||||
isPublic: boolean
|
||||
questions: Array<{ question: string; answers: Array<{ text: string; points: number }> }>
|
||||
questions: Array<{ text: string; answers: Array<{ text: string; points: number }> }>
|
||||
}): Promise<EditCardPackDto> => {
|
||||
try {
|
||||
const response = await adminApiClient.post('/api/admin/packs/import', data)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function GameQuestionEditorDialog({
|
|||
useEffect(() => {
|
||||
if (open) {
|
||||
if (question) {
|
||||
setQuestionText(question.question || '')
|
||||
setQuestionText(question.text || '')
|
||||
setAnswers(
|
||||
question.answers && question.answers.length > 0
|
||||
? [...question.answers]
|
||||
|
|
@ -108,7 +108,7 @@ export function GameQuestionEditorDialog({
|
|||
}
|
||||
|
||||
const questionData: Question = {
|
||||
question: questionText.trim(),
|
||||
text: questionText.trim(),
|
||||
answers: validAnswers,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function GameQuestionsManager({
|
|||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{question.question}</p>
|
||||
<p className="font-medium">{question.text}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{question.answers.length} answer
|
||||
{question.answers.length !== 1 ? 's' : ''} (points:{' '}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
|
|||
// Validate questions structure
|
||||
const isValid = questions.every(
|
||||
(q: any) =>
|
||||
(q.question || q.text) &&
|
||||
q.text &&
|
||||
typeof q.text === 'string' &&
|
||||
Array.isArray(q.answers) &&
|
||||
q.answers.length > 0 &&
|
||||
q.answers.every(
|
||||
|
|
@ -87,7 +88,7 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
|
|||
)
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid question format. Each question must have "question" (or "text") and "answers" array with "text" and "points" fields.')
|
||||
throw new Error('Invalid question format. Each question must have "text" field and "answers" array with "text" and "points" fields.')
|
||||
}
|
||||
|
||||
setParseError(null)
|
||||
|
|
@ -151,14 +152,18 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
|
|||
return
|
||||
}
|
||||
|
||||
// Normalize questions to use 'question' field
|
||||
const normalizedQuestions = parsed.map((q: any) => ({
|
||||
question: q.question || q.text,
|
||||
answers: q.answers.map((a: any) => ({
|
||||
text: a.text,
|
||||
points: a.points,
|
||||
})),
|
||||
}))
|
||||
// Normalize questions to use 'text' field (ignore 'question' field if present)
|
||||
const normalizedQuestions = parsed.map((q: any) => {
|
||||
const { question, ...rest } = q
|
||||
return {
|
||||
...rest,
|
||||
text: q.text || '',
|
||||
answers: q.answers.map((a: any) => ({
|
||||
text: a.text,
|
||||
points: a.points,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
|
|
@ -227,7 +232,7 @@ export function PackImportDialog({ open, onImport, onClose }: PackImportDialogPr
|
|||
id="jsonContent"
|
||||
value={jsonContent}
|
||||
onChange={(e) => handleJsonChange(e.target.value)}
|
||||
placeholder='[{"question": "...", "answers": [{"text": "...", "points": 100}]}]'
|
||||
placeholder='[{"text": "...", "answers": [{"text": "...", "points": 100}]}]'
|
||||
rows={8}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function InputButtonsQuestionForm({
|
|||
|
||||
const addButton = () => {
|
||||
const newButton: TestButton = {
|
||||
id: `btn_${Date.now()}`,
|
||||
id: crypto.randomUUID(),
|
||||
text: '',
|
||||
}
|
||||
setButtons([...buttons, newButton])
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function SimpleQuestionForm({
|
|||
|
||||
const addButton = () => {
|
||||
const newButton: TestButton = {
|
||||
id: `btn_${Date.now()}`,
|
||||
id: crypto.randomUUID(),
|
||||
text: '',
|
||||
}
|
||||
setButtons([...buttons, newButton])
|
||||
|
|
|
|||
|
|
@ -158,10 +158,10 @@ export default function PacksPage() {
|
|||
const fullPack = await packsApi.getPack(pack.id)
|
||||
setSelectedPack(fullPack)
|
||||
|
||||
// Convert questions from backend format (supports both 'question' and 'text' fields)
|
||||
// Convert questions from backend format (using 'text' field)
|
||||
const questions: Question[] = Array.isArray(fullPack.questions)
|
||||
? fullPack.questions.map((q: any) => ({
|
||||
question: q.question || q.text || '',
|
||||
text: q.text || '',
|
||||
answers: Array.isArray(q.answers)
|
||||
? q.answers.map((a: any) => ({
|
||||
text: a.text || '',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface Answer {
|
|||
}
|
||||
|
||||
export interface Question {
|
||||
question: string
|
||||
text: string
|
||||
answers: Answer[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,25 +16,26 @@ interface Answer {
|
|||
interface Question {
|
||||
id?: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
answers: Answer[];
|
||||
}
|
||||
|
||||
function ensureQuestionIds(questions: Question[]): Question[] {
|
||||
return questions.map((question) => {
|
||||
const questionId = question.id || randomUUID();
|
||||
const questionText = question.text || question.question || '';
|
||||
const questionText = question.text || '';
|
||||
|
||||
const answersWithIds = question.answers.map((answer) => ({
|
||||
...answer,
|
||||
id: answer.id || randomUUID(),
|
||||
}));
|
||||
|
||||
// Удаляем поле question если оно было в исходном объекте
|
||||
const { question: _, ...questionWithoutQuestion } = question;
|
||||
|
||||
return {
|
||||
...question,
|
||||
...questionWithoutQuestion,
|
||||
id: questionId,
|
||||
text: questionText,
|
||||
question: questionText, // Keep both fields for compatibility
|
||||
answers: answersWithIds,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ export class AdminPacksController {
|
|||
return {
|
||||
templateVersion: '1.0',
|
||||
instructions:
|
||||
'Fill in your questions below. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
|
||||
'Fill in your questions below. Each question must have a "text" field and an "answers" array with "text" and "points" fields.',
|
||||
questions: [
|
||||
{
|
||||
question: 'Your question here',
|
||||
text: 'Your question here',
|
||||
answers: [
|
||||
{ text: 'Answer 1', points: 100 },
|
||||
{ text: 'Answer 2', points: 80 },
|
||||
|
|
@ -56,8 +56,8 @@ export class AdminPacksController {
|
|||
// Validate question structure
|
||||
const isValid = importPackDto.questions.every(
|
||||
(q) =>
|
||||
q.question &&
|
||||
typeof q.question === 'string' &&
|
||||
q.text &&
|
||||
typeof q.text === 'string' &&
|
||||
Array.isArray(q.answers) &&
|
||||
q.answers.length > 0 &&
|
||||
q.answers.every(
|
||||
|
|
@ -70,7 +70,7 @@ export class AdminPacksController {
|
|||
|
||||
if (!isValid) {
|
||||
throw new BadRequestException(
|
||||
'Invalid question format. Each question must have a "question" field and an "answers" array with "text" and "points" fields.',
|
||||
'Invalid question format. Each question must have a "text" field and an "answers" array with "text" and "points" fields.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ export class AdminPacksService {
|
|||
throw new NotFoundException('Question pack not found');
|
||||
}
|
||||
|
||||
// Нормализуем вопросы при экспорте, удаляя поле question если оно есть
|
||||
const packQuestions = Array.isArray(pack.questions) ? pack.questions as any[] : [];
|
||||
const normalizedQuestions = ensureQuestionIds(packQuestions);
|
||||
|
||||
return {
|
||||
templateVersion: '1.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
|
|
@ -179,7 +183,7 @@ export class AdminPacksService {
|
|||
category: pack.category,
|
||||
isPublic: pack.isPublic,
|
||||
},
|
||||
questions: pack.questions,
|
||||
questions: normalizedQuestions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class QuestionDto {
|
|||
id?: string;
|
||||
|
||||
@IsString()
|
||||
question: string;
|
||||
text: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class ImportAnswerDto {
|
|||
|
||||
class ImportQuestionDto {
|
||||
@IsString()
|
||||
question: string;
|
||||
text: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class QuestionDto {
|
|||
id?: string;
|
||||
|
||||
@IsString()
|
||||
question: string;
|
||||
text: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ interface PlayerAction {
|
|||
interface Question {
|
||||
id: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
answers: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
|
|
@ -441,7 +440,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
}
|
||||
return {
|
||||
id: questionId || `temp-${Math.random()}`,
|
||||
text: q.text || q.question || '',
|
||||
text: q.text || '',
|
||||
answers: (q.answers || []).map((a: any) => ({
|
||||
id: a.id || `answer-${Math.random()}`,
|
||||
text: a.text || '',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ interface Answer {
|
|||
interface Question {
|
||||
id?: string;
|
||||
text?: string;
|
||||
question?: string; // Поддержка обоих вариантов названия поля
|
||||
answers: Answer[];
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ export function ensureQuestionIds(questions: Question[]): Question[] {
|
|||
return questions.map((question) => {
|
||||
// Если ID нет или не является валидным UUID, создаем новый
|
||||
const questionId = (question.id && isValidUUID(question.id)) ? question.id : randomUUID();
|
||||
const questionText = question.text || question.question || '';
|
||||
const questionText = question.text || '';
|
||||
|
||||
const answersWithIds = question.answers.map((answer) => {
|
||||
// Если ID нет или не является валидным UUID, создаем новый
|
||||
|
|
@ -45,11 +44,13 @@ export function ensureQuestionIds(questions: Question[]): Question[] {
|
|||
};
|
||||
});
|
||||
|
||||
// Удаляем поле question если оно было в исходном объекте
|
||||
const { question: _, ...questionWithoutQuestion } = question;
|
||||
|
||||
return {
|
||||
...question,
|
||||
...questionWithoutQuestion,
|
||||
id: questionId,
|
||||
text: questionText,
|
||||
question: questionText, // Сохраняем оба поля для совместимости
|
||||
answers: answersWithIds,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ interface Answer {
|
|||
interface Question {
|
||||
id: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
answers?: Answer[];
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +64,7 @@ export class VoiceService {
|
|||
}
|
||||
|
||||
if (contentType === TTSContentType.QUESTION) {
|
||||
const questionText = question.text || question.question;
|
||||
const questionText = question.text;
|
||||
if (!questionText) {
|
||||
this.logger.error(`Question text is empty for questionId=${questionId}`);
|
||||
throw new NotFoundException('Question text is empty');
|
||||
|
|
|
|||
|
|
@ -824,10 +824,102 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
|
||||
border-radius: var(--border-radius-sm, 8px);
|
||||
transition: all 0.2s;
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent-primary, #ffd700);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item.dragging {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item.drag-over {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item.drag-over-above {
|
||||
border-top: 3px solid var(--accent-primary, #ffd700);
|
||||
padding-top: calc(1rem - 1px);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item.drag-over-below {
|
||||
border-bottom: 3px solid var(--accent-primary, #ffd700);
|
||||
padding-bottom: calc(1rem - 1px);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item button:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-order {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--accent-primary, #ffd700);
|
||||
color: var(--bg-primary, #000000);
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-order-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-order-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary, #ffffff);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-order-button:hover:not(:disabled) {
|
||||
background: var(--accent-primary, #ffd700);
|
||||
color: var(--bg-primary, #000000);
|
||||
border-color: var(--accent-primary, #ffd700);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-order-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-content {
|
||||
|
|
@ -850,6 +942,33 @@
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-drag-handle {
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: grab;
|
||||
padding: 0.5rem 0.25rem;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s, transform 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
letter-spacing: -0.3rem;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item:hover .questions-modal-item-drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item.dragging .questions-modal-item-drag-handle,
|
||||
.questions-tab-content .questions-modal-item:hover .questions-modal-item-drag-handle:hover {
|
||||
color: var(--accent-primary, #ffd700);
|
||||
cursor: grabbing;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-edit-button,
|
||||
.questions-tab-content .questions-modal-delete-button {
|
||||
width: 32px;
|
||||
|
|
@ -902,6 +1021,20 @@
|
|||
.questions-tab-content .questions-modal-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-item-order {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.questions-tab-content .questions-modal-order-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
|
|
|
|||
|
|
@ -82,6 +82,11 @@ const GameManagementModal = ({
|
|||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [viewingQuestion, setViewingQuestion] = useState(null)
|
||||
const [showAnswers, setShowAnswers] = useState(false)
|
||||
|
||||
// Drag and drop state
|
||||
const [draggedQuestionIndex, setDraggedQuestionIndex] = useState(null)
|
||||
const [dragOverIndex, setDragOverIndex] = useState(null)
|
||||
const [dragOverPosition, setDragOverPosition] = useState(null) // 'above' | 'below'
|
||||
|
||||
// Сбрасываем вкладку на initialTab при открытии модального окна
|
||||
useEffect(() => {
|
||||
|
|
@ -237,11 +242,12 @@ const GameManagementModal = ({
|
|||
if (!validateQuestionForm()) return
|
||||
|
||||
const questionData = {
|
||||
id: editingQuestion ? editingQuestion.id : Date.now(),
|
||||
id: editingQuestion ? editingQuestion.id : crypto.randomUUID(),
|
||||
text: questionText.trim(),
|
||||
answers: answers
|
||||
.filter(a => a.text.trim())
|
||||
.map(a => ({
|
||||
id: a.id || crypto.randomUUID(),
|
||||
text: a.text.trim(),
|
||||
points: a.points,
|
||||
})),
|
||||
|
|
@ -270,9 +276,139 @@ const GameManagementModal = ({
|
|||
}
|
||||
}
|
||||
|
||||
const handleMoveQuestionUp = (index) => {
|
||||
if (index === 0) return
|
||||
const updatedQuestions = [...questions]
|
||||
const temp = updatedQuestions[index]
|
||||
updatedQuestions[index] = updatedQuestions[index - 1]
|
||||
updatedQuestions[index - 1] = temp
|
||||
onUpdateQuestions(updatedQuestions)
|
||||
}
|
||||
|
||||
const handleMoveQuestionDown = (index) => {
|
||||
if (index === questions.length - 1) return
|
||||
const updatedQuestions = [...questions]
|
||||
const temp = updatedQuestions[index]
|
||||
updatedQuestions[index] = updatedQuestions[index + 1]
|
||||
updatedQuestions[index + 1] = temp
|
||||
onUpdateQuestions(updatedQuestions)
|
||||
}
|
||||
|
||||
// Drag and drop handlers
|
||||
const handleDragStart = (e, index) => {
|
||||
// Предотвращаем перетаскивание при клике на кнопки или инпуты
|
||||
const target = e.target
|
||||
const clickedButton = target.tagName === 'BUTTON' || target.closest('button')
|
||||
const clickedInput = target.tagName === 'INPUT' || target.closest('input')
|
||||
|
||||
// Если кликнули на кнопку или инпут, не запускаем drag
|
||||
if (clickedButton || clickedInput) {
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
setDraggedQuestionIndex(index)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
e.dataTransfer.setData('text/plain', index.toString())
|
||||
// Находим элемент вопроса и делаем его полупрозрачным
|
||||
const item = e.currentTarget
|
||||
item.style.opacity = '0.5'
|
||||
}
|
||||
|
||||
const handleDragEnd = (e) => {
|
||||
// Восстанавливаем opacity элемента
|
||||
const item = e.currentTarget
|
||||
item.style.opacity = ''
|
||||
setDraggedQuestionIndex(null)
|
||||
setDragOverIndex(null)
|
||||
setDragOverPosition(null)
|
||||
}
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
|
||||
if (draggedQuestionIndex === null || draggedQuestionIndex === index) {
|
||||
setDragOverIndex(null)
|
||||
setDragOverPosition(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Определяем, куда вставлять элемент (выше или ниже)
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const mouseY = e.clientY
|
||||
const elementMiddleY = rect.top + rect.height / 2
|
||||
const position = mouseY < elementMiddleY ? 'above' : 'below'
|
||||
|
||||
if (dragOverIndex !== index || dragOverPosition !== position) {
|
||||
setDragOverIndex(index)
|
||||
setDragOverPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragOverIndex(null)
|
||||
setDragOverPosition(null)
|
||||
}
|
||||
|
||||
const handleDrop = (e, dropIndex) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (draggedQuestionIndex === null || draggedQuestionIndex === dropIndex) {
|
||||
setDragOverIndex(null)
|
||||
setDragOverPosition(null)
|
||||
setDraggedQuestionIndex(null)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedQuestions = [...questions]
|
||||
const draggedQuestion = updatedQuestions[draggedQuestionIndex]
|
||||
|
||||
// Определяем финальную позицию вставки на основе позиции перетаскивания
|
||||
let insertIndex = dropIndex
|
||||
if (dragOverPosition === 'below') {
|
||||
insertIndex = dropIndex + 1
|
||||
}
|
||||
|
||||
// Удаляем элемент из старой позиции
|
||||
updatedQuestions.splice(draggedQuestionIndex, 1)
|
||||
|
||||
// Корректируем индекс вставки после удаления элемента
|
||||
// Если удалили элемент выше позиции вставки, нужно уменьшить индекс на 1
|
||||
if (draggedQuestionIndex < insertIndex) {
|
||||
insertIndex -= 1
|
||||
}
|
||||
|
||||
// Ограничиваем индекс диапазоном массива
|
||||
insertIndex = Math.max(0, Math.min(insertIndex, updatedQuestions.length))
|
||||
|
||||
// Вставляем элемент в новую позицию
|
||||
updatedQuestions.splice(insertIndex, 0, draggedQuestion)
|
||||
|
||||
onUpdateQuestions(updatedQuestions)
|
||||
setDraggedQuestionIndex(null)
|
||||
setDragOverIndex(null)
|
||||
setDragOverPosition(null)
|
||||
}
|
||||
|
||||
const handleExportJson = () => {
|
||||
try {
|
||||
const jsonString = JSON.stringify(questions, null, 2)
|
||||
// Нормализуем вопросы, удаляя поле question если оно есть
|
||||
const normalizedQuestions = questions.map(q => {
|
||||
const { question, ...questionWithoutQuestion } = q
|
||||
return {
|
||||
...questionWithoutQuestion,
|
||||
text: q.text || '',
|
||||
answers: q.answers.map(a => ({
|
||||
id: a.id,
|
||||
text: a.text,
|
||||
points: a.points
|
||||
}))
|
||||
}
|
||||
})
|
||||
const jsonString = JSON.stringify(normalizedQuestions, null, 2)
|
||||
const blob = new Blob([jsonString], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
|
|
@ -361,15 +497,19 @@ const GameManagementModal = ({
|
|||
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}`
|
||||
}))
|
||||
}))
|
||||
// Нормализуем вопросы, удаляя поле question если оно есть, и добавляем id если его нет
|
||||
const questionsWithIds = jsonContent.map((q) => {
|
||||
const { question, ...rest } = q
|
||||
return {
|
||||
...rest,
|
||||
id: q.id || crypto.randomUUID(),
|
||||
text: q.text || '',
|
||||
answers: q.answers.map((a) => ({
|
||||
...a,
|
||||
id: a.id || crypto.randomUUID()
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
onUpdateQuestions(questionsWithIds)
|
||||
setJsonError('')
|
||||
|
|
@ -410,7 +550,7 @@ const GameManagementModal = ({
|
|||
// Фильтрация вопросов по поисковому запросу
|
||||
const filteredPackQuestions = packQuestions.filter((q) => {
|
||||
if (!searchQuery.trim()) return true
|
||||
const questionText = (q.text || q.question || '').toLowerCase()
|
||||
const questionText = (q.text || '').toLowerCase()
|
||||
return questionText.includes(searchQuery.toLowerCase())
|
||||
})
|
||||
|
||||
|
|
@ -476,10 +616,14 @@ const GameManagementModal = ({
|
|||
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 copiedQuestions = questionsToImport.map((q) => ({
|
||||
id: crypto.randomUUID(),
|
||||
text: q.text || '',
|
||||
answers: (q.answers || []).map(a => ({
|
||||
id: a.id || crypto.randomUUID(),
|
||||
text: a.text,
|
||||
points: a.points
|
||||
})),
|
||||
}))
|
||||
|
||||
const updatedQuestions = [...questions, ...copiedQuestions]
|
||||
|
|
@ -1061,7 +1205,7 @@ const GameManagementModal = ({
|
|||
onChange={() => handleToggleQuestion(originalIndex)}
|
||||
/>
|
||||
<div className="pack-question-content">
|
||||
<strong>{q.text || q.question}</strong>
|
||||
<strong>{q.text || ''}</strong>
|
||||
<span className="pack-question-info">
|
||||
{q.answers?.length || 0} ответов
|
||||
</span>
|
||||
|
|
@ -1096,7 +1240,7 @@ const GameManagementModal = ({
|
|||
</div>
|
||||
<div className="pack-question-viewer-content">
|
||||
<div className="pack-question-viewer-text">
|
||||
{viewingQuestion.text || viewingQuestion.question}
|
||||
{viewingQuestion.text || ''}
|
||||
</div>
|
||||
<button
|
||||
className="pack-show-answers-button"
|
||||
|
|
@ -1197,8 +1341,50 @@ const GameManagementModal = ({
|
|||
<p className="questions-modal-empty">Нет вопросов. Добавьте вопросы для игры.</p>
|
||||
) : (
|
||||
<div className="questions-modal-items">
|
||||
{questions.map((question) => (
|
||||
<div key={question.id} className="questions-modal-item">
|
||||
{questions.map((question, index) => (
|
||||
<div
|
||||
key={question.id}
|
||||
className={`questions-modal-item ${
|
||||
draggedQuestionIndex === index ? 'dragging' : ''
|
||||
} ${dragOverIndex === index ? 'drag-over' : ''} ${
|
||||
dragOverIndex === index && dragOverPosition ? `drag-over-${dragOverPosition}` : ''
|
||||
}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
>
|
||||
<div className="questions-modal-item-order">
|
||||
<span className="questions-modal-item-number">{index + 1}</span>
|
||||
<div className="questions-modal-item-order-buttons">
|
||||
<button
|
||||
className="questions-modal-order-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleMoveQuestionUp(index)
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
disabled={index === 0}
|
||||
title="Переместить вверх"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
className="questions-modal-order-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleMoveQuestionDown(index)
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
disabled={index === questions.length - 1}
|
||||
title="Переместить вниз"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="questions-modal-item-content">
|
||||
<div className="questions-modal-item-text">{question.text}</div>
|
||||
<div className="questions-modal-item-info">
|
||||
|
|
@ -1208,19 +1394,34 @@ const GameManagementModal = ({
|
|||
<div className="questions-modal-item-actions">
|
||||
<button
|
||||
className="questions-modal-edit-button"
|
||||
onClick={() => handleEditQuestion(question)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleEditQuestion(question)
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title="Редактировать"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
className="questions-modal-delete-button"
|
||||
onClick={() => handleDeleteQuestion(question.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteQuestion(question.id)
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title="Удалить"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="questions-modal-item-drag-handle"
|
||||
title="Перетащите для изменения порядка"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
⋮⋮
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const UPDATE_INTERVAL = 500 // Check every 500ms
|
|||
|
||||
function createSnowflake(id, isInitial = false) {
|
||||
return {
|
||||
id: id || `snowflake-${Date.now()}-${Math.random()}`,
|
||||
id: id || crypto.randomUUID(),
|
||||
left: Math.random() * 100,
|
||||
duration: Math.random() * 3 + 7, // 7-10s
|
||||
delay: isInitial ? Math.random() * 2 : 0, // Only delay initial batch
|
||||
|
|
|
|||
Loading…
Reference in a new issue