usmle-exam-system / src /components /QuestionManager.jsx
jianlizhao's picture
Upload 16 files
6fe52ee verified
Raw
History Blame Contribute Delete
11.6 kB
import React, { useState } from 'react'
import { Upload, Plus, Trash2, Edit2, Download } from 'lucide-react'
export default function QuestionManager({ onQuestionsImport, currentQuestions }) {
const [showImportModal, setShowImportModal] = useState(false)
const [showAddModal, setShowAddModal] = useState(false)
const [importMethod, setImportMethod] = useState('json')
const [jsonInput, setJsonInput] = useState('')
const [csvInput, setCsvInput] = useState('')
const [newQuestion, setNewQuestion] = useState({
text: '',
options: [
{ label: 'A', text: '' },
{ label: 'B', text: '' },
{ label: 'C', text: '' },
{ label: 'D', text: '' },
{ label: 'E', text: '' }
],
image: null
})
const [errorMessage, setErrorMessage] = useState('')
const [successMessage, setSuccessMessage] = useState('')
const handleJsonImport = () => {
try {
setErrorMessage('')
const parsed = JSON.parse(jsonInput)
// Validate format
if (!Array.isArray(parsed)) {
throw new Error('Questions must be an array')
}
parsed.forEach((q, idx) => {
if (!q.text || !Array.isArray(q.options) || q.options.length !== 5) {
throw new Error(`Question ${idx + 1}: Must have text and 5 options (A-E)`)
}
})
onQuestionsImport(parsed)
setSuccessMessage(`✅ Successfully imported ${parsed.length} questions!`)
setJsonInput('')
setShowImportModal(false)
setTimeout(() => setSuccessMessage(''), 3000)
} catch (err) {
setErrorMessage(`❌ Error: ${err.message}`)
}
}
const handleCsvImport = () => {
try {
setErrorMessage('')
const lines = csvInput.trim().split('\n')
const questions = []
for (let i = 1; i < lines.length; i++) {
const line = lines[i]
if (!line.trim()) continue
const parts = line.split('|').map(p => p.trim())
if (parts.length < 6) {
throw new Error(`Row ${i + 1}: Need Question|A|B|C|D|E format`)
}
questions.push({
id: i,
text: parts[0],
options: [
{ label: 'A', text: parts[1] },
{ label: 'B', text: parts[2] },
{ label: 'C', text: parts[3] },
{ label: 'D', text: parts[4] },
{ label: 'E', text: parts[5] }
],
image: parts[6] || null
})
}
onQuestionsImport(questions)
setSuccessMessage(`✅ Successfully imported ${questions.length} questions!`)
setCsvInput('')
setShowImportModal(false)
setTimeout(() => setSuccessMessage(''), 3000)
} catch (err) {
setErrorMessage(`❌ Error: ${err.message}`)
}
}
const handleAddQuestion = () => {
try {
setErrorMessage('')
if (!newQuestion.text.trim()) {
throw new Error('Question text is required')
}
if (newQuestion.options.some(opt => !opt.text.trim())) {
throw new Error('All 5 options must have text')
}
const questionToAdd = {
id: (currentQuestions?.length || 0) + 1,
...newQuestion
}
onQuestionsImport([...(currentQuestions || []), questionToAdd])
setSuccessMessage('✅ Question added successfully!')
setNewQuestion({
text: '',
options: [
{ label: 'A', text: '' },
{ label: 'B', text: '' },
{ label: 'C', text: '' },
{ label: 'D', text: '' },
{ label: 'E', text: '' }
],
image: null
})
setShowAddModal(false)
setTimeout(() => setSuccessMessage(''), 3000)
} catch (err) {
setErrorMessage(`❌ Error: ${err.message}`)
}
}
const handleDownloadTemplate = () => {
const template = [
{
id: 1,
text: "Example question text here?",
options: [
{ label: 'A', text: 'Option A text' },
{ label: 'B', text: 'Option B text' },
{ label: 'C', text: 'Option C text' },
{ label: 'D', text: 'Option D text' },
{ label: 'E', text: 'Option E text' }
],
image: null
}
]
const element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(template, null, 2)))
element.setAttribute('download', 'questions-template.json')
element.style.display = 'none'
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
}
return (
<div className="question-manager">
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h3 className="text-lg font-bold text-blue-900 mb-3">📚 Import Your Questions</h3>
<div className="flex gap-3 flex-wrap">
<button
onClick={() => setShowImportModal(true)}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload className="w-4 h-4" />
Import Questions
</button>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="w-4 h-4" />
Add Question
</button>
<button
onClick={handleDownloadTemplate}
className="flex items-center gap-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors"
>
<Download className="w-4 h-4" />
Download Template
</button>
</div>
{currentQuestions?.length > 0 && (
<p className="text-sm text-blue-700 mt-3">
✅ {currentQuestions.length} questions loaded
</p>
)}
</div>
{/* Success/Error Messages */}
{successMessage && (
<div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
{successMessage}
</div>
)}
{errorMessage && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{errorMessage}
</div>
)}
{/* Import Modal */}
{showImportModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
<h3 className="text-xl font-bold mb-4">Import Questions</h3>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Choose Format:</label>
<select
value={importMethod}
onChange={(e) => setImportMethod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
<option value="json">JSON Format</option>
<option value="csv">CSV Format</option>
</select>
</div>
{importMethod === 'json' && (
<div>
<label className="block text-sm font-semibold mb-2">Paste JSON:</label>
<textarea
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
placeholder='[{"text": "Question?", "options": [{"label": "A", "text": "..."}...], "image": null}]'
className="w-full px-3 py-2 border border-gray-300 rounded-lg h-40 font-mono text-sm"
/>
</div>
)}
{importMethod === 'csv' && (
<div>
<label className="block text-sm font-semibold mb-2">Paste CSV (Question|A|B|C|D|E|ImageURL):</label>
<textarea
value={csvInput}
onChange={(e) => setCsvInput(e.target.value)}
placeholder="Question text here?|Option A|Option B|Option C|Option D|Option E|"
className="w-full px-3 py-2 border border-gray-300 rounded-lg h-40 font-mono text-sm"
/>
</div>
)}
<div className="flex gap-3 mt-4">
<button
onClick={importMethod === 'json' ? handleJsonImport : handleCsvImport}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Import
</button>
<button
onClick={() => setShowImportModal(false)}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Add Question Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
<h3 className="text-xl font-bold mb-4">Add New Question</h3>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Question Text:</label>
<textarea
value={newQuestion.text}
onChange={(e) => setNewQuestion({ ...newQuestion, text: e.target.value })}
placeholder="Enter your question here..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg h-20"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Options:</label>
{newQuestion.options.map((opt, idx) => (
<div key={idx} className="flex gap-2 mb-2">
<span className="w-8 font-bold">{opt.label}:</span>
<input
type="text"
value={opt.text}
onChange={(e) => {
const updated = [...newQuestion.options]
updated[idx].text = e.target.value
setNewQuestion({ ...newQuestion, options: updated })
}}
placeholder={`Option ${opt.label} text`}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>
))}
</div>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Image URL (optional):</label>
<input
type="url"
value={newQuestion.image || ''}
onChange={(e) => setNewQuestion({ ...newQuestion, image: e.target.value || null })}
placeholder="https://example.com/image.jpg"
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>
<div className="flex gap-3">
<button
onClick={handleAddQuestion}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700"
>
Add Question
</button>
<button
onClick={() => setShowAddModal(false)}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
)
}