| 'use client'; | |
| import { GeneratedQuestion, QuestionType } from '@/types/quiz'; | |
| import { Card } from '@/components/ui/card'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Checkbox } from '@/components/ui/checkbox'; | |
| import { cn } from '@/lib/utils'; | |
| export interface QuestionCardProps { | |
| question: GeneratedQuestion; | |
| questionTypes: QuestionType[]; | |
| onEdit?: (question: GeneratedQuestion) => void; | |
| onDuplicate?: (question: GeneratedQuestion) => void; | |
| onPreview?: (question: GeneratedQuestion) => void; | |
| onRemove?: (questionId: string) => void; | |
| isSelected?: boolean; | |
| onSelect?: (questionId: string, selected: boolean) => void; | |
| onDragStart?: (questionId: string) => void; | |
| onDragEnd?: () => void; | |
| onDragOver?: (e: React.DragEvent) => void; | |
| onDrop?: (questionId: string) => void; | |
| isDragging?: boolean; | |
| isEditing?: boolean; | |
| className?: string; | |
| } | |
| export default function QuestionCard({ | |
| question, | |
| questionTypes, | |
| onEdit, | |
| onDuplicate, | |
| onPreview, | |
| onRemove, | |
| isSelected = false, | |
| onSelect, | |
| onDragStart, | |
| onDragEnd, | |
| onDragOver, | |
| onDrop, | |
| isDragging = false, | |
| isEditing = false, | |
| className = '', | |
| }: QuestionCardProps) { | |
| const questionType = questionTypes.find(t => t.id === question.type); | |
| const handleSelect = () => { | |
| if (onSelect) { | |
| onSelect(question.id, !isSelected); | |
| } | |
| }; | |
| const handleDragStart = (e: React.DragEvent) => { | |
| e.dataTransfer.setData('text/plain', question.id); | |
| if (onDragStart) { | |
| onDragStart(question.id); | |
| } | |
| }; | |
| const handleDragEnd = () => { | |
| if (onDragEnd) { | |
| onDragEnd(); | |
| } | |
| }; | |
| const handleDragOver = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| if (onDragOver) { | |
| onDragOver(e); | |
| } | |
| }; | |
| const handleDrop = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| const draggedId = e.dataTransfer.getData('text/plain'); | |
| if (onDrop && draggedId !== question.id) { | |
| onDrop(question.id); | |
| } | |
| }; | |
| return ( | |
| <Card | |
| draggable | |
| onDragStart={handleDragStart} | |
| onDragEnd={handleDragEnd} | |
| onDragOver={handleDragOver} | |
| onDrop={handleDrop} | |
| className={cn( | |
| "p-4 hover:bg-accent group transition-all duration-200 cursor-move", | |
| isSelected && "ring-2 ring-primary bg-primary/5", | |
| isDragging && "opacity-50 scale-95 rotate-2", | |
| className | |
| )} | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-start space-x-3 flex-1"> | |
| {/* Drag Handle */} | |
| <div className="mt-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-move"> | |
| <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"> | |
| <circle cx="2" cy="2" r="1"/> | |
| <circle cx="6" cy="2" r="1"/> | |
| <circle cx="10" cy="2" r="1"/> | |
| <circle cx="2" cy="6" r="1"/> | |
| <circle cx="6" cy="6" r="1"/> | |
| <circle cx="10" cy="6" r="1"/> | |
| <circle cx="2" cy="10" r="1"/> | |
| <circle cx="6" cy="10" r="1"/> | |
| <circle cx="10" cy="10" r="1"/> | |
| </svg> | |
| </div> | |
| {/* Selection Checkbox */} | |
| {onSelect && ( | |
| <Checkbox | |
| checked={isSelected} | |
| onCheckedChange={handleSelect} | |
| className="mt-1" | |
| /> | |
| )} | |
| {/* Question Content */} | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <span className="text-sm font-medium text-primary"> | |
| {questionType?.name || 'Unknown Type'} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {question.points} pts | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {new Date(question.createdAt).toLocaleDateString()} | |
| </span> | |
| </div> | |
| <div className="text-foreground text-sm leading-relaxed"> | |
| <p className="font-medium mb-2">{question.stem}</p> | |
| {/* Display Options */} | |
| {question.content && question.content.Options && ( | |
| <div className="mt-3 space-y-1"> | |
| {Object.entries(question.content.Options).map(([key, value]) => ( | |
| <div key={key} className="text-xs text-muted-foreground"> | |
| <span className="font-medium">{key})</span> {value as string} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Display Answer */} | |
| {question.content && question.content.Answer && ( | |
| <div className="mt-2 text-xs"> | |
| <span className="font-medium text-green-600 dark:text-green-400">Answer: {question.content.Answer}</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ml-2"> | |
| {onEdit && ( | |
| <Button | |
| onClick={() => onEdit(question)} | |
| variant={isEditing ? "default" : "ghost"} | |
| size="icon" | |
| className={cn( | |
| "h-8 w-8 transition-all duration-200", | |
| isEditing | |
| ? "bg-primary text-primary-foreground hover:bg-primary/90 shadow-md" | |
| : "hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900/30 dark:hover:text-blue-400 hover:scale-105 hover:shadow-sm" | |
| )} | |
| title={isEditing ? "Currently editing" : "Edit question"} | |
| > | |
| <span className="text-sm transition-transform duration-200 group-hover:scale-110"> | |
| โ๏ธ | |
| </span> | |
| </Button> | |
| )} | |
| {onDuplicate && ( | |
| <Button | |
| onClick={() => onDuplicate(question)} | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 transition-all duration-200 hover:bg-green-100 hover:text-green-600 dark:hover:bg-green-900/30 dark:hover:text-green-400 hover:scale-105 hover:shadow-sm" | |
| title="Duplicate question" | |
| > | |
| <span className="text-sm transition-transform duration-200 group-hover:scale-110">๐</span> | |
| </Button> | |
| )} | |
| {onPreview && ( | |
| <Button | |
| onClick={() => onPreview(question)} | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 transition-all duration-200 hover:bg-purple-100 hover:text-purple-600 dark:hover:bg-purple-900/30 dark:hover:text-purple-400 hover:scale-105 hover:shadow-sm" | |
| title="Preview question" | |
| > | |
| <span className="text-sm transition-transform duration-200 group-hover:scale-110">๐๏ธ</span> | |
| </Button> | |
| )} | |
| {onRemove && ( | |
| <Button | |
| onClick={() => onRemove(question.id)} | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 transition-all duration-200 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/30 dark:hover:text-red-400 hover:scale-105 hover:shadow-sm" | |
| title="Remove question" | |
| > | |
| <span className="text-sm transition-transform duration-200 group-hover:scale-110">โ</span> | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| </Card> | |
| ); | |
| } |