Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useState } from 'react'; | |
| import { GeneratedQuestion, QuestionType } from '@/types/quiz'; | |
| import QuestionCard from './QuestionCard'; | |
| import { exportQuestionsToDocx } from '@/lib/exportUtils'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; | |
| import { Checkbox } from '@/components/ui/checkbox'; | |
| import { Label } from '@/components/ui/label'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| export interface QuestionListProps { | |
| questions: GeneratedQuestion[]; | |
| questionTypes: QuestionType[]; | |
| articleText?: string; | |
| selectedQuestions?: Set<string>; | |
| onQuestionSelect?: (questionId: string, selected: boolean) => void; | |
| onQuestionEdit?: (question: GeneratedQuestion) => void; | |
| onQuestionDuplicate?: (question: GeneratedQuestion) => void; | |
| onQuestionPreview?: (question: GeneratedQuestion) => void; | |
| onQuestionRemove?: (questionId: string) => void; | |
| onQuestionReorder?: (questions: GeneratedQuestion[]) => void; | |
| onQuestionIncreaseDifficulty?: (question: GeneratedQuestion) => void; | |
| onQuestionDecreaseDifficulty?: (question: GeneratedQuestion) => void; | |
| editingQuestionId?: string | null; | |
| emptyStateMessage?: string; | |
| emptyStateIcon?: string; | |
| className?: string; | |
| title?: string; | |
| showCount?: boolean; | |
| } | |
| export default function QuestionList({ | |
| questions, | |
| questionTypes, | |
| articleText, | |
| selectedQuestions = new Set(), | |
| onQuestionSelect, | |
| onQuestionEdit, | |
| onQuestionDuplicate, | |
| onQuestionPreview, | |
| onQuestionRemove, | |
| onQuestionReorder, | |
| onQuestionIncreaseDifficulty, | |
| onQuestionDecreaseDifficulty, | |
| editingQuestionId = null, | |
| emptyStateMessage = "No questions yet. Start by creating your first question!", | |
| emptyStateIcon = "๐", | |
| className = '', | |
| title = "Question List", | |
| showCount = true, | |
| }: QuestionListProps) { | |
| const [draggedId, setDraggedId] = useState<string | null>(null); | |
| const [isExporting, setIsExporting] = useState(false); | |
| const [includeAnswers, setIncludeAnswers] = useState(true); | |
| const [exportFormat, setExportFormat] = useState<'docx' | 'txt'>('docx'); | |
| const handleSelectAll = () => { | |
| if (onQuestionSelect) { | |
| const allSelected = questions.length > 0 && questions.every(q => selectedQuestions.has(q.id)); | |
| questions.forEach(question => { | |
| onQuestionSelect(question.id, !allSelected); | |
| }); | |
| } | |
| }; | |
| const handleDragStart = (questionId: string) => { | |
| setDraggedId(questionId); | |
| }; | |
| const handleDragEnd = () => { | |
| setDraggedId(null); | |
| }; | |
| const handleDrop = (targetId: string) => { | |
| if (!draggedId || !onQuestionReorder || draggedId === targetId) return; | |
| const newQuestions = [...questions]; | |
| const draggedIndex = newQuestions.findIndex(q => q.id === draggedId); | |
| const targetIndex = newQuestions.findIndex(q => q.id === targetId); | |
| if (draggedIndex !== -1 && targetIndex !== -1) { | |
| // Remove the dragged item and insert it at the target position | |
| const [draggedItem] = newQuestions.splice(draggedIndex, 1); | |
| newQuestions.splice(targetIndex, 0, draggedItem); | |
| onQuestionReorder(newQuestions); | |
| } | |
| }; | |
| const allSelected = questions.length > 0 && questions.every(q => selectedQuestions.has(q.id)); | |
| const someSelected = questions.some(q => selectedQuestions.has(q.id)); | |
| return ( | |
| <Card className={className}> | |
| <CardHeader className="border-b"> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle className="text-lg font-semibold"> | |
| ๐๏ธ {title} {showCount && `(${questions.length})`} | |
| </CardTitle> | |
| {/* Export and Bulk Actions */} | |
| {questions.length > 0 && ( | |
| <div className="flex items-center space-x-3"> | |
| {/* Export Options */} | |
| <div className="flex items-center space-x-3"> | |
| {/* Format Selector */} | |
| <Select value={exportFormat} onValueChange={(value) => setExportFormat(value as 'docx' | 'txt')}> | |
| <SelectTrigger className="w-[100px]"> | |
| <SelectValue /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="docx">DOCX</SelectItem> | |
| <SelectItem value="txt">TXT</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| {/* Include Answers Toggle */} | |
| <div className="flex items-center space-x-2"> | |
| <Checkbox | |
| id="include-answers" | |
| checked={includeAnswers} | |
| onCheckedChange={(checked) => setIncludeAnswers(checked as boolean)} | |
| /> | |
| <Label htmlFor="include-answers" className="text-sm cursor-pointer"> | |
| Include Answers | |
| </Label> | |
| </div> | |
| {/* Export Button */} | |
| <Button | |
| onClick={async () => { | |
| if (isExporting) return; | |
| setIsExporting(true); | |
| try { | |
| const questionsToExport = someSelected && selectedQuestions.size > 0 | |
| ? questions.filter(q => selectedQuestions.has(q.id)) | |
| : questions; | |
| const filename = someSelected && selectedQuestions.size > 0 | |
| ? `selected-questions-${selectedQuestions.size}${includeAnswers ? '-with-answers' : '-no-answers'}.${exportFormat}` | |
| : `all-questions-${questions.length}${includeAnswers ? '-with-answers' : '-no-answers'}.${exportFormat}`; | |
| // Debug: Log the first question structure | |
| if (questionsToExport.length > 0) { | |
| console.log('First question structure:', questionsToExport[0]); | |
| console.log('First question content:', questionsToExport[0].content); | |
| } | |
| if (exportFormat === 'docx') { | |
| await exportQuestionsToDocx(questionsToExport, filename, includeAnswers, articleText); | |
| } else { | |
| // For now, just show a message that TXT export is not implemented | |
| alert('TXT export is not yet implemented. Please use DOCX format.'); | |
| } | |
| } catch (error) { | |
| console.error('Export failed:', error); | |
| // You could add a toast notification here | |
| } finally { | |
| setIsExporting(false); | |
| } | |
| }} | |
| disabled={isExporting} | |
| variant="default" | |
| size="sm" | |
| title={someSelected && selectedQuestions.size > 0 | |
| ? `Export ${selectedQuestions.size} selected questions to ${exportFormat.toUpperCase()} file` | |
| : `Export all questions to ${exportFormat.toUpperCase()} file` | |
| } | |
| > | |
| <span>{isExporting ? 'โณ' : '๐'}</span> | |
| <span> | |
| {isExporting | |
| ? 'Exporting...' | |
| : someSelected && selectedQuestions.size > 0 | |
| ? `Export Selected (${selectedQuestions.size})` | |
| : `Export to ${exportFormat.toUpperCase()}` | |
| } | |
| </span> | |
| </Button> | |
| </div> | |
| {/* Bulk Actions */} | |
| {onQuestionSelect && ( | |
| <div className="flex items-center space-x-2"> | |
| <Button | |
| onClick={handleSelectAll} | |
| variant="ghost" | |
| size="sm" | |
| > | |
| {allSelected ? 'Deselect All' : 'Select All'} | |
| </Button> | |
| {someSelected && ( | |
| <span className="text-xs text-primary"> | |
| {selectedQuestions.size} selected | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </CardHeader> | |
| {/* Content */} | |
| <CardContent className="h-[calc(100%-80px)] overflow-y-auto p-4"> | |
| {questions.length === 0 ? ( | |
| <EmptyState message={emptyStateMessage} icon={emptyStateIcon} /> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {questions.map((question) => ( | |
| <QuestionCard | |
| key={question.id} | |
| question={question} | |
| questionTypes={questionTypes} | |
| isSelected={selectedQuestions.has(question.id)} | |
| onSelect={onQuestionSelect} | |
| onEdit={onQuestionEdit} | |
| onDuplicate={onQuestionDuplicate} | |
| onPreview={onQuestionPreview} | |
| onRemove={onQuestionRemove} | |
| onIncreaseDifficulty={onQuestionIncreaseDifficulty} | |
| onDecreaseDifficulty={onQuestionDecreaseDifficulty} | |
| onDragStart={handleDragStart} | |
| onDragEnd={handleDragEnd} | |
| onDrop={handleDrop} | |
| isDragging={draggedId === question.id} | |
| isEditing={editingQuestionId === question.id} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| // Empty State Component | |
| interface EmptyStateProps { | |
| message: string; | |
| icon: string; | |
| } | |
| function EmptyState({ message, icon }: EmptyStateProps) { | |
| return ( | |
| <div className="text-center text-gray-500 dark:text-gray-400 mt-8"> | |
| <div className="text-4xl mb-4">{icon}</div> | |
| <p className="text-sm leading-relaxed max-w-md mx-auto">{message}</p> | |
| </div> | |
| ); | |
| } |