Spaces:
Runtime error
Runtime error
| 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> | |
| ) | |
| } | |