LMS-BGN/src/components/admin/EnhancedQuestionBuilder.tsx

960 lines
35 KiB
TypeScript

'use client';
import React, { useState, useRef } from 'react';
import {
Plus,
Minus,
Upload,
Image,
Video,
Music,
File,
X,
Eye,
Settings,
Puzzle,
Gamepad2,
Brain,
Timer,
Star,
Heart,
Zap,
MousePointer,
Shuffle,
Target,
CheckCircle,
AlertCircle,
HelpCircle,
Play,
Pause
} from 'lucide-react';
import { MultimediaManager, MultimediaFile } from './MultimediaManager';
export interface InteractiveQuestionOption {
id: string;
text: string;
isCorrect: boolean;
multimedia?: MultimediaFile;
explanation?: string;
weight?: number; // for weighted scoring
}
export interface InteractiveQuestionData {
id?: string;
title: string;
type: 'multiple_choice' | 'essay' | 'true_false' | 'enhanced_mcq' | 'puzzle' | 'scenario' | 'puzzle_gambar_icon' | 'true_false_cepat' | 'mini_simulation_rpg' | 'mini_survey_reflection';
category: string;
difficulty: 'easy' | 'medium' | 'hard';
points: number;
explanation?: string;
status: 'draft' | 'active';
// Standard question data
options?: InteractiveQuestionOption[];
essayAnswer?: string;
trueFalseAnswer?: boolean;
// Interactive features
multimedia?: MultimediaFile[];
interactiveConfig?: {
// Enhanced MCQ
allowMultipleAnswers?: boolean;
enableHints?: boolean;
hints?: string[];
// Puzzle features
puzzleType?: 'drag_drop' | 'sequencing' | 'matching' | 'visual_matching';
puzzleItems?: Array<{
id: string;
content: string;
position?: { x: number; y: number };
matchTarget?: string;
multimedia?: MultimediaFile;
}>;
// Scenario simulation
scenarioStages?: Array<{
id: string;
title: string;
description: string;
choices: Array<{
id: string;
text: string;
consequence: string;
nextStage?: string;
}>;
multimedia?: MultimediaFile;
}>;
// RPG simulation
rpgConfig?: {
character?: {
name: string;
attributes: { [key: string]: number };
};
storyline?: Array<{
id: string;
text: string;
choices: Array<{
id: string;
text: string;
attributeEffects?: { [key: string]: number };
nextScene?: string;
}>;
}>;
};
// Reflection survey
reflectionConfig?: {
emojiScale?: {
min: number;
max: number;
labels: string[];
};
textPrompts?: string[];
allowSkip?: boolean;
};
// Timing and behavior
timeLimit?: number;
showTimer?: boolean;
trackBehavior?: boolean;
allowReview?: boolean;
};
}
interface EnhancedQuestionBuilderProps {
initialData?: Partial<InteractiveQuestionData>;
onSave: (questionData: InteractiveQuestionData) => void;
onCancel: () => void;
categories: string[];
className?: string;
}
export const EnhancedQuestionBuilder: React.FC<EnhancedQuestionBuilderProps> = ({
initialData,
onSave,
onCancel,
categories,
className = ''
}) => {
const [questionData, setQuestionData] = useState<InteractiveQuestionData>({
title: '',
type: 'multiple_choice',
category: '',
difficulty: 'medium',
points: 10,
explanation: '',
status: 'draft',
options: [
{ id: '1', text: '', isCorrect: false },
{ id: '2', text: '', isCorrect: false },
{ id: '3', text: '', isCorrect: false },
{ id: '4', text: '', isCorrect: false }
],
multimedia: [],
interactiveConfig: {
allowMultipleAnswers: false,
enableHints: false,
hints: [],
timeLimit: 0,
showTimer: false,
trackBehavior: false,
allowReview: true
},
...initialData
});
const [activeTab, setActiveTab] = useState<'basic' | 'content' | 'interactive' | 'multimedia' | 'preview'>('basic');
const [showMultimediaManager, setShowMultimediaManager] = useState(false);
const [previewMode, setPreviewMode] = useState(false);
const questionTypes = [
{ value: 'multiple_choice', label: 'Pilihan Ganda', icon: CheckCircle, description: 'Soal pilihan ganda standar' },
{ value: 'enhanced_mcq', label: 'Pilihan Ganda Plus', icon: Star, description: 'Pilihan ganda dengan fitur tambahan' },
{ value: 'essay', label: 'Essay', icon: File, description: 'Soal essay dengan jawaban terbuka' },
{ value: 'true_false', label: 'Benar/Salah', icon: Target, description: 'Soal benar atau salah' },
{ value: 'true_false_cepat', label: 'Benar/Salah Cepat', icon: Zap, description: 'Benar/salah dengan tracking waktu reaksi' },
{ value: 'puzzle', label: 'Puzzle', icon: Puzzle, description: 'Soal puzzle interaktif' },
{ value: 'puzzle_gambar_icon', label: 'Puzzle Visual', icon: Image, description: 'Puzzle dengan gambar dan ikon' },
{ value: 'scenario', label: 'Simulasi Skenario', icon: Brain, description: 'Simulasi skenario dengan pilihan bertingkat' },
{ value: 'mini_simulation_rpg', label: 'Mini RPG', icon: Gamepad2, description: 'Simulasi RPG mini dengan karakter' },
{ value: 'mini_survey_reflection', label: 'Refleksi Survey', icon: Heart, description: 'Survey refleksi dengan skala emoji' }
];
const handleInputChange = (field: keyof InteractiveQuestionData, value: any) => {
setQuestionData(prev => ({
...prev,
[field]: value
}));
};
const handleInteractiveConfigChange = (field: string, value: any) => {
setQuestionData(prev => ({
...prev,
interactiveConfig: {
...prev.interactiveConfig,
[field]: value
}
}));
};
const handleOptionChange = (optionId: string, field: keyof InteractiveQuestionOption, value: any) => {
setQuestionData(prev => ({
...prev,
options: prev.options?.map(option =>
option.id === optionId ? { ...option, [field]: value } : option
)
}));
};
const addOption = () => {
const newId = ((questionData.options?.length || 0) + 1).toString();
setQuestionData(prev => ({
...prev,
options: [...(prev.options || []), { id: newId, text: '', isCorrect: false }]
}));
};
const removeOption = (optionId: string) => {
if ((questionData.options?.length || 0) > 2) {
setQuestionData(prev => ({
...prev,
options: prev.options?.filter(option => option.id !== optionId)
}));
}
};
const addHint = () => {
const hints = questionData.interactiveConfig?.hints || [];
handleInteractiveConfigChange('hints', [...hints, '']);
};
const updateHint = (index: number, value: string) => {
const hints = questionData.interactiveConfig?.hints || [];
const newHints = [...hints];
newHints[index] = value;
handleInteractiveConfigChange('hints', newHints);
};
const removeHint = (index: number) => {
const hints = questionData.interactiveConfig?.hints || [];
handleInteractiveConfigChange('hints', hints.filter((_, i) => i !== index));
};
const handleMultimediaUpload = async (files: File[]): Promise<MultimediaFile[]> => {
// Simulate file upload - in real implementation, this would upload to server
const uploadedFiles: MultimediaFile[] = files.map(file => ({
id: Math.random().toString(36).substr(2, 9),
name: file.name,
type: file.type.startsWith('image/') ? 'image' :
file.type.startsWith('video/') ? 'video' :
file.type.startsWith('audio/') ? 'audio' : 'document',
url: URL.createObjectURL(file),
size: file.size,
uploadedAt: new Date()
}));
setQuestionData(prev => ({
...prev,
multimedia: [...(prev.multimedia || []), ...uploadedFiles]
}));
return uploadedFiles;
};
const handleMultimediaDelete = async (fileId: string) => {
setQuestionData(prev => ({
...prev,
multimedia: prev.multimedia?.filter(file => file.id !== fileId)
}));
};
const validateQuestion = (): string[] => {
const errors: string[] = [];
if (!questionData.title.trim()) {
errors.push('Judul soal harus diisi');
}
if (!questionData.category) {
errors.push('Kategori harus dipilih');
}
if (['multiple_choice', 'enhanced_mcq'].includes(questionData.type)) {
const hasCorrectAnswer = questionData.options?.some(option => option.isCorrect);
const hasEmptyOption = questionData.options?.some(option => !option.text.trim());
if (hasEmptyOption) {
errors.push('Semua pilihan jawaban harus diisi');
}
if (!hasCorrectAnswer) {
errors.push('Pilih jawaban yang benar');
}
}
if (questionData.type === 'essay' && !questionData.essayAnswer?.trim()) {
errors.push('Kunci jawaban essay harus diisi');
}
return errors;
};
const handleSave = () => {
const errors = validateQuestion();
if (errors.length > 0) {
alert('Terdapat kesalahan:\n' + errors.join('\n'));
return;
}
onSave(questionData);
};
const renderBasicTab = () => (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Judul Soal *
</label>
<textarea
value={questionData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
rows={3}
placeholder="Masukkan pertanyaan soal..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipe Soal *
</label>
<div className="space-y-2">
{questionTypes.map(type => {
const Icon = type.icon;
return (
<label key={type.value} className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="questionType"
value={type.value}
checked={questionData.type === type.value}
onChange={(e) => handleInputChange('type', e.target.value)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<Icon className="w-5 h-5 ml-3 mr-2 text-gray-600" />
<div>
<div className="font-medium text-gray-900">{type.label}</div>
<div className="text-sm text-gray-500">{type.description}</div>
</div>
</label>
);
})}
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Kategori *
</label>
<select
value={questionData.category}
onChange={(e) => handleInputChange('category', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
>
<option value="">Pilih Kategori</option>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tingkat Kesulitan
</label>
<select
value={questionData.difficulty}
onChange={(e) => handleInputChange('difficulty', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="easy">Mudah</option>
<option value="medium">Sedang</option>
<option value="hard">Sulit</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Poin
</label>
<input
type="number"
value={questionData.points}
onChange={(e) => handleInputChange('points', parseInt(e.target.value))}
min="1"
max="100"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
value={questionData.status}
onChange={(e) => handleInputChange('status', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="draft">Draft</option>
<option value="active">Aktif</option>
</select>
</div>
</div>
</div>
</div>
);
const renderContentTab = () => (
<div className="space-y-6">
{/* Standard Multiple Choice */}
{['multiple_choice', 'enhanced_mcq'].includes(questionData.type) && (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Pilihan Jawaban</h3>
<div className="space-y-4">
{questionData.options?.map((option, index) => (
<div key={option.id} className="flex items-start space-x-3 p-4 border rounded-lg">
<input
type={questionData.interactiveConfig?.allowMultipleAnswers ? "checkbox" : "radio"}
name="correctAnswer"
checked={option.isCorrect}
onChange={() => {
if (questionData.interactiveConfig?.allowMultipleAnswers) {
handleOptionChange(option.id, 'isCorrect', !option.isCorrect);
} else {
// Single answer - uncheck others
setQuestionData(prev => ({
...prev,
options: prev.options?.map(opt => ({
...opt,
isCorrect: opt.id === option.id
}))
}));
}
}}
className="mt-1 w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1 space-y-2">
<input
type="text"
value={option.text}
onChange={(e) => handleOptionChange(option.id, 'text', e.target.value)}
placeholder={`Pilihan ${String.fromCharCode(65 + index)}`}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{questionData.type === 'enhanced_mcq' && (
<input
type="text"
value={option.explanation || ''}
onChange={(e) => handleOptionChange(option.id, 'explanation', e.target.value)}
placeholder="Penjelasan untuk pilihan ini (opsional)"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
)}
</div>
{(questionData.options?.length || 0) > 2 && (
<button
type="button"
onClick={() => removeOption(option.id)}
className="text-red-600 hover:text-red-800 p-1"
>
<X className="w-4 h-4" />
</button>
)}
</div>
))}
<button
type="button"
onClick={addOption}
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm font-medium"
>
<Plus className="w-4 h-4" />
Tambah Pilihan
</button>
</div>
</div>
)}
{/* Essay */}
{questionData.type === 'essay' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Kunci Jawaban / Rubrik Penilaian
</label>
<textarea
value={questionData.essayAnswer || ''}
onChange={(e) => handleInputChange('essayAnswer', e.target.value)}
rows={5}
placeholder="Masukkan kunci jawaban atau rubrik penilaian..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
)}
{/* True/False */}
{['true_false', 'true_false_cepat'].includes(questionData.type) && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Jawaban yang Benar
</label>
<div className="flex space-x-4">
<label className="flex items-center">
<input
type="radio"
name="trueFalseAnswer"
checked={questionData.trueFalseAnswer === true}
onChange={() => handleInputChange('trueFalseAnswer', true)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Benar</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="trueFalseAnswer"
checked={questionData.trueFalseAnswer === false}
onChange={() => handleInputChange('trueFalseAnswer', false)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Salah</span>
</label>
</div>
</div>
)}
{/* Explanation */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Penjelasan (Opsional)
</label>
<textarea
value={questionData.explanation || ''}
onChange={(e) => handleInputChange('explanation', e.target.value)}
rows={4}
placeholder="Berikan penjelasan untuk jawaban yang benar..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
);
const renderInteractiveTab = () => (
<div className="space-y-6">
{/* Enhanced MCQ Features */}
{questionData.type === 'enhanced_mcq' && (
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Fitur Enhanced MCQ</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="flex items-center">
<input
type="checkbox"
checked={questionData.interactiveConfig?.allowMultipleAnswers || false}
onChange={(e) => handleInteractiveConfigChange('allowMultipleAnswers', e.target.checked)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Izinkan Multiple Answers</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={questionData.interactiveConfig?.enableHints || false}
onChange={(e) => handleInteractiveConfigChange('enableHints', e.target.checked)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Aktifkan Hints</span>
</label>
</div>
{/* Hints Management */}
{questionData.interactiveConfig?.enableHints && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Hints</h4>
<div className="space-y-2">
{questionData.interactiveConfig.hints?.map((hint, index) => (
<div key={index} className="flex items-center gap-2">
<input
type="text"
value={hint}
onChange={(e) => updateHint(index, e.target.value)}
placeholder={`Hint ${index + 1}`}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={() => removeHint(index)}
className="text-red-600 hover:text-red-800 p-1"
>
<X className="w-4 h-4" />
</button>
</div>
))}
<button
type="button"
onClick={addHint}
className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm font-medium"
>
<Plus className="w-4 h-4" />
Tambah Hint
</button>
</div>
</div>
)}
</div>
)}
{/* Timing and Behavior */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Pengaturan Waktu & Perilaku</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Batas Waktu (detik, 0 = tidak terbatas)
</label>
<input
type="number"
value={questionData.interactiveConfig?.timeLimit || 0}
onChange={(e) => handleInteractiveConfigChange('timeLimit', parseInt(e.target.value))}
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
checked={questionData.interactiveConfig?.showTimer || false}
onChange={(e) => handleInteractiveConfigChange('showTimer', e.target.checked)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Tampilkan Timer</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={questionData.interactiveConfig?.trackBehavior || false}
onChange={(e) => handleInteractiveConfigChange('trackBehavior', e.target.checked)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Lacak Perilaku</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={questionData.interactiveConfig?.allowReview || false}
onChange={(e) => handleInteractiveConfigChange('allowReview', e.target.checked)}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">Izinkan Review</span>
</label>
</div>
</div>
</div>
{/* Interactive Type Specific Settings */}
{questionData.type === 'true_false_cepat' && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-medium text-yellow-800 mb-2">Pengaturan True/False Cepat</h4>
<p className="text-sm text-yellow-700">
Soal ini akan melacak waktu reaksi siswa dan memberikan feedback berdasarkan kecepatan jawaban.
</p>
</div>
)}
{questionData.type === 'mini_survey_reflection' && (
<div className="space-y-4">
<h4 className="font-medium text-gray-900">Pengaturan Survey Refleksi</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Skala Emoji Min
</label>
<input
type="number"
value={questionData.interactiveConfig?.reflectionConfig?.emojiScale?.min || 1}
onChange={(e) => handleInteractiveConfigChange('reflectionConfig', {
...questionData.interactiveConfig?.reflectionConfig,
emojiScale: {
...questionData.interactiveConfig?.reflectionConfig?.emojiScale,
min: parseInt(e.target.value)
}
})}
min="1"
max="5"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Skala Emoji Max
</label>
<input
type="number"
value={questionData.interactiveConfig?.reflectionConfig?.emojiScale?.max || 4}
onChange={(e) => handleInteractiveConfigChange('reflectionConfig', {
...questionData.interactiveConfig?.reflectionConfig,
emojiScale: {
...questionData.interactiveConfig?.reflectionConfig?.emojiScale,
max: parseInt(e.target.value)
}
})}
min="2"
max="10"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
)}
</div>
);
const renderMultimediaTab = () => (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">Multimedia Files</h3>
<button
onClick={() => setShowMultimediaManager(!showMultimediaManager)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload className="w-4 h-4" />
{showMultimediaManager ? 'Tutup' : 'Kelola'} Multimedia
</button>
</div>
{showMultimediaManager && (
<MultimediaManager
files={questionData.multimedia || []}
onFileUpload={handleMultimediaUpload}
onFileDelete={handleMultimediaDelete}
className="border-2 border-dashed border-gray-300 rounded-lg"
/>
)}
{/* Display current multimedia files */}
{(questionData.multimedia?.length || 0) > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-3">File Terlampir ({questionData.multimedia?.length})</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{questionData.multimedia?.map(file => {
const Icon = file.type === 'image' ? Image :
file.type === 'video' ? Video :
file.type === 'audio' ? Music : File;
return (
<div key={file.id} className="border rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Icon className="w-4 h-4 text-gray-600" />
<span className="text-sm font-medium truncate">{file.name}</span>
</div>
{file.type === 'image' && (
<img src={file.url} alt={file.name} className="w-full h-20 object-cover rounded" />
)}
<div className="text-xs text-gray-500 mt-1">
{(file.size / 1024).toFixed(1)} KB
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
const renderPreviewTab = () => (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">Preview Soal</h3>
<button
onClick={() => setPreviewMode(!previewMode)}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<Eye className="w-4 h-4" />
{previewMode ? 'Edit Mode' : 'Preview Mode'}
</button>
</div>
<div className="border-2 border-gray-200 rounded-lg p-6 bg-gray-50">
<div className="bg-white rounded-lg p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded">
{questionTypes.find(t => t.value === questionData.type)?.label}
</span>
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs font-medium rounded">
{questionData.difficulty === 'easy' ? 'Mudah' :
questionData.difficulty === 'medium' ? 'Sedang' : 'Sulit'}
</span>
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded">
{questionData.points} poin
</span>
</div>
{questionData.interactiveConfig?.timeLimit && questionData.interactiveConfig.timeLimit > 0 && (
<div className="flex items-center gap-1 text-sm text-gray-600">
<Timer className="w-4 h-4" />
{questionData.interactiveConfig.timeLimit}s
</div>
)}
</div>
<h4 className="text-lg font-medium text-gray-900 mb-4">
{questionData.title || 'Judul soal akan muncul di sini...'}
</h4>
{/* Preview based on question type */}
{['multiple_choice', 'enhanced_mcq'].includes(questionData.type) && (
<div className="space-y-3">
{questionData.options?.map((option, index) => (
<label key={option.id} className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type={questionData.interactiveConfig?.allowMultipleAnswers ? "checkbox" : "radio"}
name="preview"
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
disabled
/>
<span className="ml-3">{option.text || `Pilihan ${String.fromCharCode(65 + index)}`}</span>
{option.isCorrect && (
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
)}
</label>
))}
</div>
)}
{questionData.type === 'essay' && (
<textarea
placeholder="Area untuk jawaban essay siswa..."
rows={5}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
disabled
/>
)}
{['true_false', 'true_false_cepat'].includes(questionData.type) && (
<div className="flex gap-4">
<label className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input type="radio" name="preview" className="w-4 h-4 text-blue-600" disabled />
<span className="ml-3">Benar</span>
{questionData.trueFalseAnswer === true && (
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
)}
</label>
<label className="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input type="radio" name="preview" className="w-4 h-4 text-blue-600" disabled />
<span className="ml-3">Salah</span>
{questionData.trueFalseAnswer === false && (
<CheckCircle className="w-4 h-4 text-green-600 ml-auto" />
)}
</label>
</div>
)}
{questionData.explanation && (
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm font-medium text-yellow-800 mb-1">Penjelasan:</p>
<p className="text-sm text-yellow-700">{questionData.explanation}</p>
</div>
)}
{/* Interactive features preview */}
{questionData.interactiveConfig?.enableHints && questionData.interactiveConfig.hints && questionData.interactiveConfig.hints.length > 0 && (
<div className="mt-4">
<button className="flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm">
<HelpCircle className="w-4 h-4" />
Lihat Hint ({questionData.interactiveConfig.hints.length})
</button>
</div>
)}
</div>
</div>
</div>
);
const tabs = [
{ id: 'basic', label: 'Informasi Dasar', icon: Settings },
{ id: 'content', label: 'Konten Soal', icon: File },
{ id: 'interactive', label: 'Fitur Interaktif', icon: Zap },
{ id: 'multimedia', label: 'Multimedia', icon: Image },
{ id: 'preview', label: 'Preview', icon: Eye }
];
return (
<div className={`bg-white rounded-lg shadow-sm border ${className}`}>
{/* Header */}
<div className="p-6 border-b">
<h2 className="text-xl font-semibold text-gray-900">Enhanced Question Builder</h2>
<p className="text-gray-600 mt-1">Buat soal interaktif dengan fitur multimedia dan konfigurasi lanjutan</p>
</div>
{/* Tabs */}
<div className="border-b">
<nav className="flex space-x-8 px-6">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
<div className="p-6">
{activeTab === 'basic' && renderBasicTab()}
{activeTab === 'content' && renderContentTab()}
{activeTab === 'interactive' && renderInteractiveTab()}
{activeTab === 'multimedia' && renderMultimediaTab()}
{activeTab === 'preview' && renderPreviewTab()}
</div>
{/* Footer Actions */}
<div className="p-6 border-t bg-gray-50 flex justify-end space-x-4">
<button
onClick={onCancel}
className="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
Batal
</button>
<button
onClick={handleSave}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Simpan Soal
</button>
</div>
</div>
);
};