QuestionAppService

This commit is contained in:
Sedat ÖZTÜRK 2025-10-16 22:17:40 +03:00
parent 7fdf4627d0
commit fa13d879ae
6 changed files with 300 additions and 461 deletions

View file

@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kurs.Platform.Entities;
using Kurs.Platform.Questions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using static Kurs.Platform.Data.Seeds.SeedConsts;
@ -15,12 +20,112 @@ public class QuestionAppService : CrudAppService<
Guid,
PagedAndSortedResultRequestDto>
{
public QuestionAppService(IRepository<Question, Guid> repo) : base(repo)
private readonly IRepository<Question, Guid> _questionRepository;
private readonly IRepository<QuestionOption, Guid> _questionOptionRepository;
public QuestionAppService(
IRepository<Question, Guid> questionRepository,
IRepository<QuestionOption, Guid> questionOptionRepository
) : base(questionRepository)
{
_questionRepository = questionRepository;
_questionOptionRepository = questionOptionRepository;
GetPolicyName = AppCodes.Definitions.Question;
GetListPolicyName = AppCodes.Definitions.Question;
CreatePolicyName = AppCodes.Definitions.Question + ".Create";
UpdatePolicyName = AppCodes.Definitions.Question + ".Update";
DeletePolicyName = AppCodes.Definitions.Question + ".Delete";
}
public override async Task<QuestionDto> GetAsync(Guid id)
{
var queryable = await _questionRepository.GetQueryableAsync();
var entity = await queryable
.Include(q => q.Options)
.FirstOrDefaultAsync(q => q.Id == id);
if (entity == null)
throw new EntityNotFoundException(typeof(Question), id);
return ObjectMapper.Map<Question, QuestionDto>(entity);
}
public override async Task<QuestionDto> UpdateAsync(Guid id, QuestionDto input)
{
var entity = await (await _questionRepository.GetDbSetAsync())
.Include(q => q.Options)
.FirstOrDefaultAsync(q => q.Id == id);
if (entity == null)
throw new EntityNotFoundException(typeof(Question), id);
// 🟦 Ana alanları güncelle
entity.Title = input.Title;
entity.Content = input.Content;
entity.MediaUrl = input.MediaUrl;
entity.MediaType = input.MediaType;
entity.Points = input.Points;
entity.Difficulty = input.Difficulty;
entity.TimeLimit = input.TimeLimit;
entity.Explanation = input.Explanation;
entity.CorrectAnswer = input.CorrectAnswer;
entity.QuestionType = input.QuestionType;
// 🟨 Şık tiplerine göre davranış belirle
var multiOptionTypes = new[] { "multiple-choice", "multiple-answer", "true-false" };
// 🔸 Eğer çoklu şık tipi değilse, sadece 1 option olacak
if (!multiOptionTypes.Contains(input.QuestionType))
{
entity.Options.Clear();
// Sadece CorrectAnswer bilgisinden tek option oluştur
if (!string.IsNullOrWhiteSpace(input.CorrectAnswer))
{
var option = new QuestionOption(
Guid.NewGuid(),
entity.Id,
input.CorrectAnswer,
true // tek seçenek, doğru kabul edilir
);
entity.Options.Add(option);
entity.CorrectAnswer = option.Id.ToString(); // CorrectAnswer alanına option Id'si yazılır
}
}
else
{
// 🔹 Çoktan seçmeli türlerde mevcut mantık devam eder
var existingOptions = entity.Options.ToList();
var incomingOptions = input.Options ?? new List<QuestionOptionDto>();
// Silinecekleri bul
var toDelete = existingOptions.Where(e => !incomingOptions.Any(i => i.Id == e.Id)).ToList();
foreach (var del in toDelete)
entity.Options.Remove(del);
// Güncelle / ekle
foreach (var optDto in incomingOptions)
{
var existing = existingOptions.FirstOrDefault(o => o.Id == optDto.Id);
if (existing != null)
{
existing.Text = optDto.Text;
existing.IsCorrect = optDto.IsCorrect;
}
else
{
entity.Options.Add(new QuestionOption(optDto.Id, entity.Id, optDto.Text, optDto.IsCorrect));
}
}
}
// 🟢 Kaydet
await _questionRepository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<Question, QuestionDto>(entity);
}
}

View file

@ -9,5 +9,6 @@ public class QuestionAutoMapperProfile : Profile
public QuestionAutoMapperProfile()
{
CreateMap<Question, QuestionDto>().ReverseMap();
CreateMap<QuestionOption, QuestionOptionDto>().ReverseMap();
}
}

View file

@ -16,4 +16,16 @@ public class QuestionOption : FullAuditedEntity<Guid>, IMultiTenant
public Question Question { get; set; }
Guid? IMultiTenant.TenantId => TenantId;
// 🟢 EF Core ve ABP için parametresiz constructor ZORUNLU
protected QuestionOption() { }
// 🟢 Yeni kayıt oluşturmak için custom constructor
public QuestionOption(Guid id, Guid questionId, string text, bool isCorrect)
{
Id = id; // burada atayabilirsin çünkü ctor içinde protected set erişilebilir
QuestionId = questionId;
Text = text;
IsCorrect = isCorrect;
}
}

View file

@ -72,7 +72,6 @@ export class MenuService {
export const getMenus = async (skipCount = 0, maxResultCount = 1000, sorting = 'order') => {
const menuService = new MenuService()
const tenant = useStoreState((state) => state.auth.tenant)
return await menuService.getList(
{

View file

@ -1,12 +1,25 @@
import { FullAuditedEntityDto } from "@/proxy";
export type QuestionType =
| "multiple-choice"
| "fill-blank"
| "multiple-answer"
| "matching"
| "ordering"
| "open-ended"
| "true-false"
| "calculation";
| 'multiple-choice'
| 'fill-blank'
| 'multiple-answer'
| 'matching'
| 'ordering'
| 'open-ended'
| 'true-false'
| 'calculation'
export const QUESTION_TYPE_LABELS: Record<QuestionType, string> = {
'multiple-choice': 'Multiple Choice',
'fill-blank': 'Fill in the Blank',
'multiple-answer': 'Multiple Answer',
'matching': 'Matching',
'ordering': 'Ordering',
'open-ended': 'Open Ended',
'true-false': 'True / False',
'calculation': 'Calculation',
}
export type ExamType = "exam" | "assignment" | "test";
export type TestType = 'pdf' | 'image';
@ -14,19 +27,15 @@ export type MediaType = 'image' | 'video';
export type QuestionDifficulty = 'easy' | 'medium' | 'hard';
export type ExamSessionStatus = 'in-progress' | 'completed' | 'submitted';
export interface QuestionPoolDto {
id: string;
export interface QuestionPoolDto extends FullAuditedEntityDto {
name: string;
description: string;
questions: QuestionDto[];
tags: string[];
createdBy: string;
creationTime: Date;
}
export interface QuestionDto {
id: string;
type: QuestionType;
export interface QuestionDto extends FullAuditedEntityDto {
questionType: QuestionType;
title: string;
content: string;
mediaUrl?: string;
@ -36,14 +45,10 @@ export interface QuestionDto {
points: number;
timeLimit?: number;
explanation?: string;
tags: string[];
difficulty: QuestionDifficulty;
creationTime: Date;
lastModificationTime: Date;
}
export interface QuestionOptionDto {
id: string;
export interface QuestionOptionDto extends FullAuditedEntityDto {
text: string;
isCorrect: boolean;
order?: number;

View file

@ -1,10 +1,5 @@
import { questionService } from '@/services/question.service'
import {
QuestionDto,
QuestionType,
QuestionDifficulty,
QuestionOptionDto,
} from '@/types/coordinator'
import { QUESTION_TYPE_LABELS, QuestionDto, QuestionOptionDto } from '@/types/coordinator'
import React, { useState, useEffect } from 'react'
import { FaSave, FaPlus, FaTrash, FaTimes } from 'react-icons/fa'
@ -15,150 +10,155 @@ function QuestionDialog({
}: {
open: boolean
onDialogClose: () => void
id: string
id?: string
}) {
const [question, setQuestion] = useState<QuestionDto>()
const [formData, setFormData] = useState({
type: 'multiple-choice' as QuestionType,
title: '',
content: '',
mediaUrl: '',
mediaType: 'image' as 'image' | 'video',
points: 10,
timeLimit: 0,
explanation: '',
difficulty: 'medium' as QuestionDifficulty,
})
const [options, setOptions] = useState<QuestionOptionDto[]>([])
const [correctAnswer, setCorrectAnswer] = useState<string | string[]>('')
const [tagInput, setTagInput] = useState('')
const [question, setQuestion] = useState<QuestionDto | null>(null)
useEffect(() => {
const fetchQuestion = async () => {
if (open) {
if (!open) return
if (id) {
const entity = await questionService.getQuestion(id)
setQuestion(entity)
} else {
setQuestion({
id: '',
questionType: 'multiple-choice',
points: 10,
title: '',
content: '',
mediaUrl: '',
mediaType: 'image',
correctAnswer: '',
difficulty: 'medium',
timeLimit: 0,
explanation: '',
options: [],
})
}
}
fetchQuestion()
}, [open])
}, [open, id])
useEffect(() => {
if (question) {
setFormData({
type: question.type,
title: question.title,
content: question.content,
mediaUrl: question.mediaUrl || '',
mediaType: question.mediaType || 'image',
points: question.points,
timeLimit: question.timeLimit || 0,
explanation: question.explanation || '',
difficulty: question.difficulty,
})
setOptions(question.options || [])
setCorrectAnswer(question.correctAnswer || '')
}
}, [question])
if (!open || !question) return null
const handleInputChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }))
// 🔧 Ortak alan değişimi
const handleChange = (field: keyof QuestionDto, value: any) => {
setQuestion((prev) => (prev ? { ...prev, [field]: value } : prev))
}
// 🔹 Option işlemleri
const addOption = () => {
const newOption: QuestionOptionDto = {
id: `opt-${Date.now()}`,
id: crypto.randomUUID(),
text: '',
isCorrect: false,
order: options.length,
order: question.options?.length || 0,
}
setOptions((prev) => [...prev, newOption])
setQuestion((prev) =>
prev ? { ...prev, options: [...(prev.options || []), newOption] } : prev,
)
}
const updateOption = (index: number, field: string, value: any) => {
setOptions((prev) => prev.map((opt, i) => (i === index ? { ...opt, [field]: value } : opt)))
const updateOption = (index: number, field: keyof QuestionOptionDto, value: any) => {
setQuestion((prev) => {
if (!prev) return prev
const newOpts = [...(prev.options || [])]
newOpts[index] = { ...newOpts[index], [field]: value }
return { ...prev, options: newOpts }
})
}
const removeOption = (index: number) => {
setOptions((prev) => prev.filter((_, i) => i !== index))
setQuestion((prev) => {
if (!prev) return prev
return { ...prev, options: prev.options?.filter((_, i) => i !== index) }
})
}
const handleSave = () => {
if (!formData.title.trim() || !formData.content.trim()) {
// 💾 Kaydetme
const handleSave = async () => {
if (!question.title.trim() || !question.content.trim()) {
alert('Please fill in the title and content fields.')
return
}
if (formData.points <= 0) {
if (question.points <= 0) {
alert('Points must be greater than 0.')
return
}
// Validate based on question type
if (['multiple-choice', 'multiple-answer'].includes(formData.type) && options.length < 2) {
if (
['multiple-choice', 'multiple-answer'].includes(question.questionType) &&
(question.options?.length || 0) < 2
) {
alert('Please add at least 2 options.')
return
}
if (formData.type === 'multiple-choice' && !options.some((opt) => opt.isCorrect)) {
if (
question.questionType === 'multiple-choice' &&
!question.options?.some((opt) => opt.isCorrect)
) {
alert('Please mark the correct answer.')
return
}
const questionData = {
...formData,
options: ['multiple-choice', 'multiple-answer', 'matching', 'ordering'].includes(
formData.type,
)
? options
: undefined,
correctAnswer: getCorrectAnswer(),
const dataToSave: QuestionDto = {
...question,
correctAnswer: getCorrectAnswer(question),
}
// onSave(questionData)
try {
if (question.id) await questionService.updateQuestion(question.id, dataToSave)
else await questionService.createQuestion(dataToSave)
onDialogClose()
} catch (err) {
console.error(err)
alert('Error while saving question.')
}
}
const getCorrectAnswer = (): string | string[] => {
switch (formData.type) {
const getCorrectAnswer = (q: QuestionDto): string | string[] => {
switch (q.questionType) {
case 'multiple-choice':
return options.find((opt) => opt.isCorrect)?.id || ''
return q.options?.find((opt) => opt.isCorrect)?.id || ''
case 'multiple-answer':
return options.filter((opt) => opt.isCorrect).map((opt) => opt.id)
return q.options?.filter((opt) => opt.isCorrect).map((opt) => opt.id!) || []
case 'true-false':
return correctAnswer as string
case 'fill-blank':
case 'open-ended':
case 'calculation':
return correctAnswer as string
return q.correctAnswer as string
case 'matching':
return options.map((opt) => opt.id)
return q.options?.map((opt) => opt.id!) || []
case 'ordering':
return options.sort((a, b) => (a.order || 0) - (b.order || 0)).map((opt) => opt.id)
return (
q.options?.sort((a, b) => (a.order || 0) - (b.order || 0)).map((opt) => opt.id!) || []
)
default:
return correctAnswer as string
return q.correctAnswer as string
}
}
const renderQuestionTypeSpecificFields = () => {
switch (formData.type) {
switch (question.questionType) {
case 'multiple-choice':
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">Yanıtlar (A, B, C, D, E)</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-3.5 h-3.5" />
<span>Şık Ekle</span>
<span>Seçenek Ekle</span>
</button>
</div>
{options.map((option, index) => (
{question.options?.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
@ -170,23 +170,24 @@ function QuestionDialog({
type="radio"
name="correct-answer"
checked={option.isCorrect}
onChange={(e) => {
// For single choice, uncheck others
setOptions((prev) =>
prev.map((opt, i) => ({
onChange={() =>
setQuestion((prev) => {
if (!prev) return prev
const updated = prev.options?.map((opt, i) => ({
...opt,
isCorrect: i === index ? e.target.checked : false,
})),
)
}}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
isCorrect: i === index,
}))
return { ...prev, options: updated }
})
}
className="h-4 w-4 text-blue-600 border-gray-300"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, 'text', e.target.value)}
placeholder={`${String.fromCharCode(65 + index)} şıkkı`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1"
/>
<button
type="button"
@ -204,9 +205,6 @@ function QuestionDialog({
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-gray-900">
Yanıtlar (Birden fazla doğru seçilebilir)
</h4>
<button
type="button"
onClick={addOption}
@ -217,7 +215,7 @@ function QuestionDialog({
</button>
</div>
{options.map((option, index) => (
{question.options?.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
@ -226,16 +224,15 @@ function QuestionDialog({
type="checkbox"
checked={option.isCorrect}
onChange={(e) => updateOption(index, 'isCorrect', e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
className="h-4 w-4 text-blue-600 border-gray-300"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, 'text', e.target.value)}
placeholder={`Yanıt ${index + 1}`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1"
/>
<div className="text-sm text-gray-600">{option.isCorrect ? 'Doğru' : 'Yanlış'}</div>
<button
type="button"
onClick={() => removeOption(index)}
@ -253,219 +250,71 @@ function QuestionDialog({
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Doğru Cevap</label>
<div className="flex space-x-4">
<label className="flex items-center">
{['true', 'false'].map((val) => (
<label key={val} className="flex items-center">
<input
type="radio"
name="true-false"
value="true"
checked={correctAnswer === 'true'}
onChange={(e) => setCorrectAnswer(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
value={val}
checked={question.correctAnswer === val}
onChange={(e) => handleChange('correctAnswer', e.target.value)}
className="h-4 w-4 text-blue-600 border-gray-300"
/>
<span className="ml-2">Doğru</span>
<span className="ml-2">{val === 'true' ? 'Doğru' : 'Yanlış'}</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="true-false"
value="false"
checked={correctAnswer === 'false'}
onChange={(e) => setCorrectAnswer(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="ml-2">Yanlış</span>
</label>
</div>
</div>
)
case 'fill-blank':
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">
Boşluk Cevapları (Maksimum 10 boşluk)
</h4>
</div>
<div className="space-y-3">
{Array.from({ length: 10 }, (_, index) => {
const blankAnswers = ((correctAnswer as string) || '').split('|')
return (
<div key={index} className="flex items-center space-x-3">
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1 rounded min-w-16 text-center">
Boşluk {index + 1}
</div>
<input
type="text"
value={blankAnswers[index] || ''}
onChange={(e) => {
const newAnswers = [...blankAnswers]
newAnswers[index] = e.target.value
// Remove empty answers from the end
while (newAnswers.length > 0 && !newAnswers[newAnswers.length - 1]) {
newAnswers.pop()
}
setCorrectAnswer(newAnswers.join('|'))
}}
placeholder={`${index + 1}. boşluk için kelime/cümle`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
)
})}
</div>
<p className="text-xs text-gray-500 mt-2">
Soru içeriğinde _____ veya [blank] kullanarak boşlukları işaretleyin
</p>
</div>
)
case 'matching':
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">Eşleştirme Çiftleri</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-4 h-4" />
<span>Çift Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-3 p-3 border border-gray-200 rounded-lg"
>
<input
type="text"
value={option.text.split('|')[0] || ''}
onChange={(e) => {
const rightSide = option.text.split('|')[1] || ''
updateOption(index, 'text', `${e.target.value}|${rightSide}`)
}}
placeholder="Sol taraf (örn: PLUS)"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<span className="text-gray-400"></span>
<input
type="text"
value={option.text.split('|')[1] || ''}
onChange={(e) => {
const leftSide = option.text.split('|')[0] || ''
updateOption(index, 'text', `${leftSide}|${e.target.value}`)
}}
placeholder="Sağ taraf (örn: ARTI)"
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
</div>
)
case 'ordering':
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-md font-medium text-gray-900">Sıralanacak Öğeler</h4>
<button
type="button"
onClick={addOption}
className="flex items-center space-x-1 text-blue-600 hover:text-blue-700"
>
<FaPlus className="w-4 h-4" />
<span>Öğe Ekle</span>
</button>
</div>
{options.map((option, index) => (
<div
key={option.id}
className="flex items-center space-x-3 p-3 border border-gray-200 rounded-lg"
>
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded min-w-16 text-center">
Sıra
</div>
<input
type="number"
value={option.order || index + 1}
onChange={(e) => updateOption(index, 'order', parseInt(e.target.value))}
min="1"
className="w-20 text-sm border border-gray-300 rounded-lg px-2 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={option.text}
onChange={(e) => updateOption(index, 'text', e.target.value)}
placeholder={`Öğe ${index + 1} (örn: I, TAKE, A SHOWER)`}
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => removeOption(index)}
className="text-red-500 hover:text-red-700"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
))}
<p className="text-xs text-gray-500 mt-2">
Öğrenciler bu öğeleri doğru sıraya göre düzenleyecek (drag & drop veya butonlarla)
</p>
</div>
)
case 'open-ended':
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Örnek Cevap (Opsiyonel)
</label>
<textarea
value={correctAnswer as string}
onChange={(e) => setCorrectAnswer(e.target.value)}
placeholder="Örnek bir cevap veya anahtar noktalar yazın..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
<p className="text-xs text-gray-500 mt-1">Öğrenci bu soruya ıklama yazabilecek</p>
</div>
)
case 'calculation':
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Doğru Cevap (Sadece sayısal sonuç)
</label>
<input
type="text"
value={correctAnswer as string}
onChange={(e) => setCorrectAnswer(e.target.value)}
placeholder="Örn: 3, 15.5, 42"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">
Öğrenci matematiksel hesaplama yapıp sayısal sonuç yazacak
</p>
</div>
)
default:
return null
// 🔹 Diğer tüm tipler: tek radio + text gösterilir
// Eğer hiç option yoksa bir tane oluştur
if (!question.options || question.options.length === 0) {
setQuestion((prev) =>
prev
? {
...prev,
options: [
{
id: crypto.randomUUID(),
text: prev.correctAnswer || 'Doğru cevap metni',
isCorrect: true,
order: 0,
},
],
}
: prev,
)
}
return (
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700 mb-2">
Doğru Cevap (Tek Seçenek)
</label>
{question.options?.map((option) => (
<div
key={option.id}
className="flex items-center space-x-3 p-2.5 border border-gray-200 rounded-lg"
>
<input
type="radio"
name="single-correct"
value={option.id}
checked={question.correctAnswer === option.id}
onChange={() => handleChange('correctAnswer', option.id)}
className="h-4 w-4 text-blue-600 border-gray-300"
/>
<span className="text-sm text-gray-800">{option.text}</span>
</div>
))}
<p className="text-xs text-gray-500 mt-1">
Bu soru tipinde yalnızca tek doğru cevap seçeneği bulunur.
</p>
</div>
)
}
}
@ -473,156 +322,24 @@ function QuestionDialog({
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-5 py-3 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
{question ? 'Edit Question' : 'Create New Question'}
</h2>
<h2 className="text-lg font-semibold text-gray-900">{question.title}</h2>
<button onClick={onDialogClose} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="p-5 space-y-4">
{/* Basic Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Question Type
</label>
<select
value={formData.type}
onChange={(e) => handleInputChange('type', e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="multiple-choice">Multiple Choice</option>
<option value="fill-blank">Fill in the Blank</option>
<option value="multiple-answer">Multiple Answer</option>
<option value="matching">Matching</option>
<option value="ordering">Ordering</option>
<option value="open-ended">Open Ended</option>
<option value="true-false">True/False</option>
<option value="calculation">Calculation</option>
</select>
</div>
<div className="p-5 space-y-4">{renderQuestionTypeSpecificFields()}</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Points</label>
<input
type="number"
value={formData.points}
onChange={(e) => handleInputChange('points', parseInt(e.target.value))}
min="1"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Question Title</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
placeholder="Enter question title..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Question Content</label>
<textarea
value={formData.content}
onChange={(e) => handleInputChange('content', e.target.value)}
placeholder="Enter the question content..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
</div>
{/* Media Upload */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Media URL (Optional)
</label>
<input
type="url"
value={formData.mediaUrl}
onChange={(e) => handleInputChange('mediaUrl', e.target.value)}
placeholder="https://example.com/image.jpg"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">Media Type</label>
<select
value={formData.mediaType}
onChange={(e) => handleInputChange('mediaType', e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="image">Image</option>
<option value="video">Video</option>
</select>
</div>
</div>
{/* Question Type Specific Fields */}
{renderQuestionTypeSpecificFields()}
{/* Additional Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Difficulty</label>
<select
value={formData.difficulty}
onChange={(e) => handleInputChange('difficulty', e.target.value)}
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Time Limit (minutes, 0 = no limit)
</label>
<input
type="number"
value={formData.timeLimit}
onChange={(e) => handleInputChange('timeLimit', parseInt(e.target.value))}
min="0"
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Explanation (Optional)
</label>
<textarea
value={formData.explanation}
onChange={(e) => handleInputChange('explanation', e.target.value)}
placeholder="Provide an explanation for the correct answer..."
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
/>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-5 py-3 flex justify-end space-x-2">
<button
onClick={onDialogClose}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium transition-colors"
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
className="flex items-center space-x-2 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg"
>
<FaSave className="w-3.5 h-3.5" />
<span>Save Question</span>