Add ability to edit AI generated content

This commit is contained in:
Joey Yakimowich-Payne 2026-01-13 23:37:08 -07:00
commit bfbba7b5ab
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
15 changed files with 1089 additions and 10 deletions

View 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>
);
};