sto-k-odnomu/admin/src/components/JSONQuestionEditor.tsx

183 lines
5.7 KiB
TypeScript
Raw Normal View History

2026-01-06 20:12:36 +00:00
import { useState, useEffect } from 'react'
import type { Question } from '@/types/questions'
import { questionToJson, questionFromJson } from '@/types/questions'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
interface JSONQuestionEditorProps {
question: Partial<Question> | null
onChange: (question: Partial<Question> | null, isValid: boolean) => void
}
export function JSONQuestionEditor({
question,
onChange,
}: JSONQuestionEditorProps) {
const [jsonText, setJsonText] = useState('')
const [error, setError] = useState<string | null>(null)
const [isValid, setIsValid] = useState(false)
// Инициализация JSON из вопроса
useEffect(() => {
if (question) {
try {
const json = questionToJson(question as Question)
setJsonText(JSON.stringify(json, null, 2))
setError(null)
setIsValid(true)
} catch (e) {
setError(`Failed to serialize question: ${e}`)
setIsValid(false)
}
} else {
setJsonText('')
setError(null)
setIsValid(false)
}
}, [question])
// Валидация и парсинг JSON
const handleJsonChange = (value: string) => {
setJsonText(value)
if (!value.trim()) {
setError(null)
setIsValid(false)
onChange(null, false)
return
}
try {
const parsed: unknown = JSON.parse(value)
if (!parsed || typeof parsed !== 'object') {
throw new Error('JSON must be an object')
}
const obj = parsed as Record<string, unknown>
// Базовая валидация структуры
if (typeof obj.questionType !== 'string') {
throw new Error('Missing required field: questionType')
}
const isMatrix = obj.questionType === 'matrix'
if (!isMatrix && !obj.word) {
throw new Error('Missing required field: word')
}
if (!isMatrix && !obj.answer) {
throw new Error('Missing required field: answer')
}
if (!isMatrix && (!obj.buttons || !Array.isArray(obj.buttons))) {
throw new Error('Missing or invalid field: buttons (must be an array)')
}
if (isMatrix && obj.matrixSize == null) {
throw new Error('Missing required field for matrix: matrixSize')
}
// Проверка что answer существует в buttons
if (!isMatrix) {
const buttons = Array.isArray(obj.buttons) ? obj.buttons : []
const buttonIds = buttons
.map((b) => {
const bObj: Record<string, unknown> =
b && typeof b === 'object' ? (b as Record<string, unknown>) : {}
return typeof bObj.id === 'string' ? bObj.id : undefined
})
.filter((id): id is string => typeof id === 'string')
if (typeof obj.answer === 'string' && !buttonIds.includes(obj.answer)) {
throw new Error(
`Answer "${obj.answer}" not found in buttons. Available IDs: ${buttonIds.join(', ')}`,
)
}
}
// Проверка для input_buttons
if (obj.questionType === 'input_buttons' && !obj.template) {
throw new Error('Missing required field for input_buttons: template')
}
// Парсинг в Question объект
const questionObj = questionFromJson(obj)
setError(null)
setIsValid(true)
onChange(questionObj, true)
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Invalid JSON'
setError(errorMessage)
setIsValid(false)
onChange(null, false)
}
}
// Форматирование JSON
const formatJson = () => {
try {
const parsed = JSON.parse(jsonText)
setJsonText(JSON.stringify(parsed, null, 2))
handleJsonChange(JSON.stringify(parsed, null, 2))
} catch {
// Если невалидный JSON, просто показываем ошибку
}
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>JSON Editor</Label>
<Button type="button" variant="outline" size="sm" onClick={formatJson}>
Format JSON
</Button>
</div>
<Textarea
value={jsonText}
onChange={(e) => handleJsonChange(e.target.value)}
placeholder='{"questionType": "simple", "word": "...", "answer": "...", "buttons": [...]}'
rows={20}
className="font-mono text-sm"
/>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{isValid && !error && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>JSON is valid</AlertDescription>
</Alert>
)}
<div className="text-xs text-muted-foreground">
<p className="font-semibold mb-1">Required fields:</p>
<ul className="list-disc list-inside space-y-1">
<li>
<code>questionType</code>: "simple" or "input_buttons"
</li>
<li>
<code>questionType</code>: "matrix" (requires <code>matrixSize</code>)
</li>
<li>
<code>word</code>: Word or phrase to learn
</li>
<li>
<code>answer</code>: ID of the correct button
</li>
<li>
<code>buttons</code>: Array of button objects with id, text/image
</li>
<li>
<code>template</code>: Required for input_buttons type
</li>
</ul>
</div>
</div>
)
}