255 lines
8.9 KiB
TypeScript
255 lines
8.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { X, Plus, Trash2, Triangle, Diamond, Circle, Square, Clock } from 'lucide-react';
|
|
import { Question, AnswerOption } from '../types';
|
|
|
|
interface QuestionEditModalProps {
|
|
question: Question;
|
|
onSave: (question: Question) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const ShapeIcon: React.FC<{ shape: string; className?: string; size?: number }> = ({ shape, className, size = 20 }) => {
|
|
const props = { size, className };
|
|
switch (shape) {
|
|
case 'triangle': return <Triangle {...props} />;
|
|
case 'diamond': return <Diamond {...props} />;
|
|
case 'circle': return <Circle {...props} />;
|
|
case 'square': return <Square {...props} />;
|
|
default: return <Circle {...props} />;
|
|
}
|
|
};
|
|
|
|
const colorMap: Record<string, string> = {
|
|
red: 'bg-red-500 border-red-500',
|
|
blue: 'bg-blue-500 border-blue-500',
|
|
yellow: 'bg-yellow-500 border-yellow-500',
|
|
green: 'bg-green-500 border-green-500'
|
|
};
|
|
|
|
const shapes = ['triangle', 'diamond', 'circle', 'square'] as const;
|
|
const colors = ['red', 'blue', 'yellow', 'green'] as const;
|
|
|
|
export const QuestionEditModal: React.FC<QuestionEditModalProps> = ({
|
|
question,
|
|
onSave,
|
|
onClose
|
|
}) => {
|
|
const [text, setText] = useState(question.text);
|
|
const [options, setOptions] = useState<AnswerOption[]>(question.options);
|
|
const [timeLimit, setTimeLimit] = useState(question.timeLimit);
|
|
|
|
const handleOptionTextChange = (index: number, newText: string) => {
|
|
setOptions(prev => prev.map((opt, i) =>
|
|
i === index ? { ...opt, text: newText } : opt
|
|
));
|
|
};
|
|
|
|
const handleReasonChange = (index: number, reason: string) => {
|
|
setOptions(prev => prev.map((opt, i) =>
|
|
i === index ? { ...opt, reason } : opt
|
|
));
|
|
};
|
|
|
|
const handleCorrectChange = (index: number) => {
|
|
setOptions(prev => prev.map((opt, i) => ({
|
|
...opt,
|
|
isCorrect: i === index
|
|
})));
|
|
};
|
|
|
|
const handleDeleteOption = (index: number) => {
|
|
if (options.length <= 2) return;
|
|
|
|
const deletedWasCorrect = options[index].isCorrect;
|
|
const newOptions = options.filter((_, i) => i !== index);
|
|
|
|
if (deletedWasCorrect && newOptions.length > 0) {
|
|
newOptions[0].isCorrect = true;
|
|
}
|
|
|
|
setOptions(newOptions.map((opt, i) => ({
|
|
...opt,
|
|
shape: shapes[i % 4],
|
|
color: colors[i % 4]
|
|
})));
|
|
};
|
|
|
|
const handleAddOption = () => {
|
|
if (options.length >= 6) return;
|
|
|
|
const newIndex = options.length;
|
|
setOptions(prev => [...prev, {
|
|
text: '',
|
|
isCorrect: false,
|
|
shape: shapes[newIndex % 4],
|
|
color: colors[newIndex % 4],
|
|
reason: ''
|
|
}]);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!text.trim()) return;
|
|
if (options.some(o => !o.text.trim())) return;
|
|
if (!options.some(o => o.isCorrect)) return;
|
|
|
|
onSave({
|
|
...question,
|
|
text: text.trim(),
|
|
options,
|
|
timeLimit
|
|
});
|
|
};
|
|
|
|
const isValid = text.trim() &&
|
|
options.every(o => o.text.trim()) &&
|
|
options.some(o => o.isCorrect) &&
|
|
options.length >= 2;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
|
onClick={onClose}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
className="bg-white rounded-2xl max-w-2xl w-full shadow-xl max-h-[90vh] overflow-hidden flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="p-6 border-b border-gray-100 flex items-center justify-between">
|
|
<h2 className="text-2xl font-black text-gray-900">Edit Question</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-gray-100 rounded-xl transition"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 overflow-y-auto flex-1 space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-bold text-gray-600 mb-2">Question Text</label>
|
|
<textarea
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
className="w-full p-4 border-2 border-gray-200 rounded-xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all resize-none font-medium"
|
|
rows={3}
|
|
placeholder="Enter your question..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<label className="text-sm font-bold text-gray-600 flex items-center gap-2">
|
|
<Clock size={18} />
|
|
Time Limit
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="range"
|
|
min="5"
|
|
max="60"
|
|
value={timeLimit}
|
|
onChange={(e) => setTimeLimit(Number(e.target.value))}
|
|
className="w-32 accent-theme-primary"
|
|
/>
|
|
<span className="font-bold text-theme-primary w-12">{timeLimit}s</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<label className="text-sm font-bold text-gray-600">
|
|
Options ({options.length}/6) - Select the correct answer
|
|
</label>
|
|
{options.length < 6 && (
|
|
<button
|
|
onClick={handleAddOption}
|
|
className="text-sm font-bold text-theme-primary hover:underline flex items-center gap-1"
|
|
>
|
|
<Plus size={16} /> Add Option
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{options.map((option, index) => (
|
|
<div
|
|
key={index}
|
|
className={`p-4 rounded-xl border-2 transition-all ${
|
|
option.isCorrect
|
|
? 'border-green-500 bg-green-50'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<button
|
|
onClick={() => handleCorrectChange(index)}
|
|
className={`mt-1 w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 transition-all ${
|
|
option.isCorrect
|
|
? colorMap[option.color]
|
|
: 'bg-gray-200 hover:bg-gray-300'
|
|
}`}
|
|
>
|
|
<ShapeIcon
|
|
shape={option.shape}
|
|
className={option.isCorrect ? 'text-white fill-white' : 'text-gray-500'}
|
|
size={16}
|
|
/>
|
|
</button>
|
|
|
|
<div className="flex-1 space-y-2">
|
|
<input
|
|
type="text"
|
|
value={option.text}
|
|
onChange={(e) => handleOptionTextChange(index, e.target.value)}
|
|
className="w-full p-2 border-2 border-gray-200 rounded-lg focus:border-theme-primary outline-none font-medium"
|
|
placeholder={`Option ${index + 1}`}
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={option.reason || ''}
|
|
onChange={(e) => handleReasonChange(index, e.target.value)}
|
|
className="w-full p-2 border-2 border-gray-100 rounded-lg focus:border-theme-primary outline-none text-sm text-gray-600"
|
|
placeholder="Explanation (optional)"
|
|
/>
|
|
</div>
|
|
|
|
{options.length > 2 && (
|
|
<button
|
|
onClick={() => handleDeleteOption(index)}
|
|
className="mt-1 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition"
|
|
>
|
|
<Trash2 size={18} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 border-t border-gray-100 flex gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="flex-1 py-3 rounded-xl font-bold border-2 border-gray-200 text-gray-600 hover:bg-gray-50 transition"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={!isValid}
|
|
className="flex-1 py-3 rounded-xl font-bold bg-theme-primary text-white hover:bg-theme-primary/90 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
};
|