Add ability to edit AI generated content
This commit is contained in:
parent
846ba2a69c
commit
bfbba7b5ab
15 changed files with 1089 additions and 10 deletions
255
components/QuestionEditModal.tsx
Normal file
255
components/QuestionEditModal.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue