lms/resources/js/pages/dashboard/exams/partials/tabs-content/questions.tsx
2025-12-15 12:26:23 +01:00

249 lines
12 KiB
TypeScript

import DataSortModal from '@/components/data-sort-modal';
import QuestionTypeBadge from '@/components/exam/question-type-badge';
import DeleteModal from '@/components/inertia/delete-modal';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { router, usePage } from '@inertiajs/react';
import { ArrowRight, ArrowUpDown, CheckCircle2, Circle, CircleCheck, Copy, Edit, HelpCircle, Plus, Trash2 } from 'lucide-react';
import { Renderer } from 'richtor';
import 'richtor/styles';
import { ExamUpdateProps } from '../../update';
import QuestionDialog from '../question-dialog';
const Questions = () => {
const { props } = usePage<ExamUpdateProps>();
const { exam } = props;
const { questions } = exam;
const handleDuplicateQuestion = (questionId: number) => {
router.post(
route('exam-questions.duplicate', { question: questionId }),
{},
{
preserveScroll: true,
},
);
};
if (questions.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="mb-4 rounded-full bg-gray-100 p-6">
<HelpCircle className="h-12 w-12 text-gray-400" />
</div>
<h3 className="mb-2 text-xl font-semibold text-gray-900">No Questions Yet</h3>
<p className="mb-6 max-w-md text-center text-gray-600">
Start building your exam by adding questions. You can create multiple choice, short answer, and many other question types.
</p>
<QuestionDialog
exam={exam}
handler={
<Button>
<Plus className="h-4 w-4" />
Add First Question
</Button>
}
/>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Exam Questions</h3>
<p className="text-sm text-gray-600">
{questions.length} {questions.length === 1 ? 'question' : 'questions'} Total: {exam.total_marks} marks
</p>
</div>
<div className="flex items-center gap-2">
<DataSortModal
title="Questions"
data={questions}
handler={
<Button variant="outline" className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
Reorder
</Button>
}
onOrderChange={(newOrder, setOpen) => {
router.post(
route('exam-questions.reorder'),
{
sortedData: newOrder,
},
{
preserveScroll: true,
onSuccess: () => setOpen && setOpen(false),
},
);
}}
renderContent={(item) => (
<Card className="flex w-full items-center justify-between px-4 py-3">
<p>{item.title}</p>
</Card>
)}
/>
<QuestionDialog
exam={exam}
handler={
<Button>
<Plus className="h-4 w-4" />
Add Question
</Button>
}
/>
</div>
</div>
<div className="space-y-3">
{questions.map((question, index) => (
<Card key={question.id} className="transition-shadow hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div className="mb-1 flex items-center gap-2">
<span className="text-sm font-medium text-gray-500">Q{index + 1}</span>
<QuestionTypeBadge type={question.question_type} />
<span className="text-sm font-medium text-blue-600">{question.marks} marks</span>
</div>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
className="bg-muted hover:bg-muted-foreground/10 h-8 w-8 rounded-full p-0"
onClick={() => handleDuplicateQuestion(question.id as number)}
>
<Copy className="h-4 w-4" />
</Button>
<QuestionDialog
exam={exam}
question={question}
handler={
<Button size="icon" variant="ghost" className="bg-muted hover:bg-muted-foreground/10 h-8 w-8 rounded-full p-0">
<Edit className="h-4 w-4" />
</Button>
}
/>
<DeleteModal
message="Are you sure you want to delete this question?"
routePath={route('exam-questions.destroy', question.id)}
actionComponent={
<Button size="icon" variant="ghost" className="bg-destructive/8 hover:bg-destructive/6 h-8 w-8 rounded-full p-0">
<Trash2 className="text-destructive text-sm" />
</Button>
}
/>
</div>
</div>
<h4 className="mb-1 font-medium text-gray-900">{question.title}</h4>
<Renderer value={question.description || ''} />
{/* Show options for multiple choice/select */}
{(question.question_type === 'multiple_choice' || question.question_type === 'multiple_select') &&
question.question_options &&
question.question_options.length > 0 && (
<div className="mt-3 flex flex-wrap items-center gap-x-6 gap-y-3">
{question.question_options.map((option) => (
<div key={option.id} className="flex items-center gap-2 text-sm">
{option.is_correct ? (
<CircleCheck strokeWidth={3} className="h-4 w-4 text-green-500" />
) : (
<Circle strokeWidth={3} className="h-4 w-4 text-gray-300" />
)}
<span className={option.is_correct ? 'font-medium text-green-700' : 'text-gray-600'}>{option.option_text}</span>
</div>
))}
</div>
)}
{/* Show matching pairs */}
{question.question_type === 'matching' && question.options?.matches && question.options.matches.length > 0 && (
<div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-500">Matching Pairs:</p>
<div className="grid gap-2 sm:grid-cols-2">
{question.options.matches.map((match: any, idx: number) => (
<div key={idx} className="flex items-center gap-2 rounded-md bg-gray-50 p-2 text-sm">
<span className="text-gray-700">{match.question}</span>
<ArrowRight className="h-3 w-3 text-gray-400" />
<span className="font-medium text-green-600">{match.answer}</span>
</div>
))}
</div>
</div>
)}
{/* Show fill blank answers */}
{question.question_type === 'fill_blank' && question.options?.answers && question.options.answers.length > 0 && (
<div className="mt-3">
<p className="mb-1 text-xs font-medium text-gray-500">Accepted Answers:</p>
<div className="flex flex-wrap gap-2">
{question.options.answers.map((answer: string, idx: number) => (
<span
key={idx}
className="inline-flex items-center gap-1 rounded-md bg-green-50 px-2 py-1 text-sm font-medium text-green-700"
>
<CheckCircle2 className="h-3 w-3" />
{answer}
</span>
))}
</div>
</div>
)}
{/* Show ordering items */}
{question.question_type === 'ordering' && question.options?.items && question.options.items.length > 0 && (
<div className="mt-3">
<p className="mb-1 text-xs font-medium text-gray-500">Correct Order:</p>
<ol className="list-inside list-decimal space-y-1 text-sm text-gray-700">
{question.options.items.map((item: string, idx: number) => (
<li key={idx}>{item}</li>
))}
</ol>
</div>
)}
{/* Show short answer sample */}
{question.question_type === 'short_answer' && question.options?.sample_answer && (
<div className="mt-3">
<p className="mb-1 text-xs font-medium text-gray-500">Guidelines:</p>
<p className="rounded-md bg-gray-50 p-2 text-sm text-gray-700">{question.options.sample_answer}</p>
</div>
)}
{/* Show listening info */}
{question.question_type === 'listening' && (
<div className="mt-3 space-y-2">
{question.options?.audio_url && (
<audio controls className="h-11 w-full">
<source src={question.options.audio_url} type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
)}
{question.options?.instructions && (
<div>
<p className="mb-1 text-xs font-medium text-gray-500">Instructions:</p>
<p className="text-sm text-gray-700">{question.options.instructions}</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default Questions;