Exam ve Pdf Test Route URL
This commit is contained in:
parent
cce976e158
commit
0dc6c7b521
3 changed files with 283 additions and 238 deletions
|
|
@ -12644,7 +12644,7 @@
|
|||
{
|
||||
"key": "admin.coordinator.examDetail",
|
||||
"path": "/admin/coordinator/exam/:id",
|
||||
"componentPath": "@/views/coordinator/Exams",
|
||||
"componentPath": "@/views/coordinator/ExamInterface/ExamInterface",
|
||||
"routeType": "protected",
|
||||
"authority": ["App.Coordinator.Exams"]
|
||||
},
|
||||
|
|
@ -12658,7 +12658,7 @@
|
|||
{
|
||||
"key": "admin.coordinator.assignmentDetail",
|
||||
"path": "/admin/coordinator/assignment/:id",
|
||||
"componentPath": "@/views/coordinator/Assignments",
|
||||
"componentPath": "@/views/coordinator/ExamInterface/ExamInterface",
|
||||
"routeType": "protected",
|
||||
"authority": ["App.Coordinator.Assignments"]
|
||||
},
|
||||
|
|
@ -12672,7 +12672,7 @@
|
|||
{
|
||||
"key": "admin.coordinator.testDetail",
|
||||
"path": "/admin/coordinator/test/:id",
|
||||
"componentPath": "@/views/coordinator/Tests",
|
||||
"componentPath": "@/views/coordinator/ExamInterface/PDFTestInterface",
|
||||
"routeType": "protected",
|
||||
"authority": ["App.Coordinator.Tests"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,40 +1,54 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { QuestionRenderer } from './QuestionRenderer';
|
||||
import { ExamNavigation } from './ExamNavigation';
|
||||
import { ExamTimer } from './ExamTimer';
|
||||
import { SecurityWarning } from './SecurityWarning';
|
||||
import { Exam, ExamSession, StudentAnswer } from '@/types/coordinator';
|
||||
import { useExamSecurity } from '@/utils/hooks/useExamSecurity';
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { QuestionRenderer } from './QuestionRenderer'
|
||||
import { ExamNavigation } from './ExamNavigation'
|
||||
import { ExamTimer } from './ExamTimer'
|
||||
import { SecurityWarning } from './SecurityWarning'
|
||||
import { ExamSession, StudentAnswer } from '@/types/coordinator'
|
||||
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { generateMockExam } from '@/mocks/mockExams'
|
||||
|
||||
interface StudentExamInterfaceProps {
|
||||
exam: Exam;
|
||||
studentId: string;
|
||||
onExamComplete: (session: ExamSession) => void;
|
||||
onExamSave?: (session: ExamSession) => void;
|
||||
const ExamInterface: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const userId = useStoreState((state) => state.auth.user.id)
|
||||
|
||||
// TODO: Replace with actual API call to fetch exam by id
|
||||
const exam = generateMockExam().find((e) => e.id === id)
|
||||
|
||||
if (!exam) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Bulunamadı</h2>
|
||||
<p className="text-gray-600 mb-6">İstenen sınav bulunamadı veya erişim yetkiniz yok.</p>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Geri Dön
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
||||
exam,
|
||||
studentId,
|
||||
onExamComplete,
|
||||
onExamSave
|
||||
}) => {
|
||||
const [session, setSession] = useState<ExamSession>({
|
||||
id: `session-${Date.now()}`,
|
||||
examId: exam.id,
|
||||
studentId,
|
||||
studentId: userId,
|
||||
startTime: new Date(),
|
||||
answers: [],
|
||||
status: 'in-progress',
|
||||
timeRemaining: exam.timeLimit * 60
|
||||
});
|
||||
timeRemaining: exam.timeLimit * 60,
|
||||
})
|
||||
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
|
||||
const [securityWarning, setSecurityWarning] = useState<{
|
||||
show: boolean;
|
||||
message: string;
|
||||
type: 'warning' | 'error' | 'info';
|
||||
}>({ show: false, message: '', type: 'warning' });
|
||||
show: boolean
|
||||
message: string
|
||||
type: 'warning' | 'error' | 'info'
|
||||
}>({ show: false, message: '', type: 'warning' })
|
||||
|
||||
// Security configuration
|
||||
const securityConfig = {
|
||||
|
|
@ -42,121 +56,150 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
|||
disableCopyPaste: true,
|
||||
disableDevTools: true,
|
||||
fullScreenMode: true,
|
||||
preventTabSwitch: true
|
||||
};
|
||||
preventTabSwitch: true,
|
||||
}
|
||||
|
||||
useExamSecurity(securityConfig, session.status === 'in-progress');
|
||||
useExamSecurity(securityConfig, session.status === 'in-progress')
|
||||
|
||||
// Save exam session to backend/localStorage
|
||||
const saveExamSession = (sessionData: ExamSession) => {
|
||||
// TODO: Replace with actual API call
|
||||
console.log('Saving exam session:', sessionData)
|
||||
localStorage.setItem(`exam-session-${sessionData.examId}`, JSON.stringify(sessionData))
|
||||
}
|
||||
|
||||
// Complete exam and navigate
|
||||
const handleExamComplete = (sessionData: ExamSession) => {
|
||||
// TODO: Replace with actual API call
|
||||
console.log('Exam completed:', sessionData)
|
||||
saveExamSession(sessionData)
|
||||
// Navigate to results or dashboard
|
||||
// navigate('/admin/coordinator/exams')
|
||||
}
|
||||
|
||||
// Auto-save mechanism
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (session.status === 'in-progress' && onExamSave) {
|
||||
onExamSave(session);
|
||||
if (session.status === 'in-progress') {
|
||||
saveExamSession(session)
|
||||
}
|
||||
}, 30000); // Save every 30 seconds
|
||||
}, 30000) // Save every 30 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [session, onExamSave]);
|
||||
return () => clearInterval(interval)
|
||||
}, [session])
|
||||
|
||||
const handleAnswerChange = (questionId: string, answer: string | string[]) => {
|
||||
setSession(prev => {
|
||||
const existingAnswerIndex = prev.answers.findIndex(a => a.questionId === questionId);
|
||||
setSession((prev) => {
|
||||
const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === questionId)
|
||||
const newAnswer: StudentAnswer = {
|
||||
questionId,
|
||||
answer,
|
||||
timeSpent: 0 // In a real app, track time spent per question
|
||||
};
|
||||
timeSpent: 0, // In a real app, track time spent per question
|
||||
}
|
||||
|
||||
const newAnswers = existingAnswerIndex >= 0
|
||||
? prev.answers.map((a, i) => i === existingAnswerIndex ? newAnswer : a)
|
||||
: [...prev.answers, newAnswer];
|
||||
const newAnswers =
|
||||
existingAnswerIndex >= 0
|
||||
? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
|
||||
: [...prev.answers, newAnswer]
|
||||
|
||||
return {
|
||||
...prev,
|
||||
answers: newAnswers
|
||||
};
|
||||
});
|
||||
};
|
||||
answers: newAnswers,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTimeUp = () => {
|
||||
setSecurityWarning({
|
||||
show: true,
|
||||
message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...',
|
||||
type: 'error'
|
||||
});
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
completeExam();
|
||||
}, 3000);
|
||||
};
|
||||
completeExam()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const completeExam = () => {
|
||||
const completedSession: ExamSession = {
|
||||
...session,
|
||||
endTime: new Date(),
|
||||
status: 'completed'
|
||||
};
|
||||
status: 'completed',
|
||||
}
|
||||
|
||||
setSession(completedSession);
|
||||
onExamComplete(completedSession);
|
||||
};
|
||||
setSession(completedSession)
|
||||
handleExamComplete(completedSession)
|
||||
}
|
||||
|
||||
const handleSubmitExam = () => {
|
||||
const unanswered = exam.questions.filter(q =>
|
||||
!session.answers.find(a => a.questionId === q.id && a.answer)
|
||||
);
|
||||
const unanswered = exam.questions.filter(
|
||||
(q) => !session.answers.find((a) => a.questionId === q.id && a.answer),
|
||||
)
|
||||
|
||||
if (unanswered.length > 0) {
|
||||
const confirmSubmit = window.confirm(
|
||||
`${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`
|
||||
);
|
||||
`${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`,
|
||||
)
|
||||
|
||||
if (!confirmSubmit) return;
|
||||
if (!confirmSubmit) return
|
||||
}
|
||||
|
||||
completeExam();
|
||||
};
|
||||
completeExam()
|
||||
}
|
||||
|
||||
const currentQuestion = exam.questions[currentQuestionIndex];
|
||||
const currentAnswer = session.answers.find(a => a.questionId === currentQuestion?.id);
|
||||
const currentQuestion = exam.questions[currentQuestionIndex]
|
||||
const currentAnswer = session.answers.find((a) => a.questionId === currentQuestion?.id)
|
||||
|
||||
const navigateQuestion = (direction: 'next' | 'prev' | number) => {
|
||||
if (typeof direction === 'number') {
|
||||
setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction)));
|
||||
setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction)))
|
||||
} else if (direction === 'next') {
|
||||
setCurrentQuestionIndex(prev => Math.min(exam.questions.length - 1, prev + 1));
|
||||
setCurrentQuestionIndex((prev) => Math.min(exam.questions.length - 1, prev + 1))
|
||||
} else {
|
||||
setCurrentQuestionIndex(prev => Math.max(0, prev - 1));
|
||||
setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (session.status === 'completed') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
className="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra bilgilendirileceksiniz.
|
||||
Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
|
||||
bilgilendirileceksiniz.
|
||||
</p>
|
||||
<div className="text-sm text-gray-500">
|
||||
Başlama: {session.startTime.toLocaleString('tr-TR')}<br />
|
||||
Başlama: {session.startTime.toLocaleString('tr-TR')}
|
||||
<br />
|
||||
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SecurityWarning
|
||||
isVisible={securityWarning.show}
|
||||
onDismiss={() => setSecurityWarning(prev => ({ ...prev, show: false }))}
|
||||
onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
|
||||
message={securityWarning.message}
|
||||
type={securityWarning.type}
|
||||
/>
|
||||
|
|
@ -172,11 +215,7 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<ExamTimer
|
||||
initialTime={exam.timeLimit * 60}
|
||||
onTimeUp={handleTimeUp}
|
||||
autoStart={true}
|
||||
/>
|
||||
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
|
||||
|
||||
<button
|
||||
onClick={handleSubmitExam}
|
||||
|
|
@ -210,7 +249,12 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
|||
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-400 text-gray-700 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Önceki</span>
|
||||
</button>
|
||||
|
|
@ -228,7 +272,12 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
|||
>
|
||||
<span>Sonraki</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -246,5 +295,7 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ExamInterface
|
||||
|
|
@ -1,41 +1,55 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { ExamTimer } from "./ExamTimer";
|
||||
import { SecurityWarning } from "./SecurityWarning";
|
||||
import { FaFileAlt, FaImage, FaCheckCircle } from "react-icons/fa";
|
||||
import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from "@/types/coordinator";
|
||||
import { useExamSecurity } from "@/utils/hooks/useExamSecurity";
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { ExamTimer } from './ExamTimer'
|
||||
import { SecurityWarning } from './SecurityWarning'
|
||||
import { FaFileAlt, FaImage, FaCheckCircle } from 'react-icons/fa'
|
||||
import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from '@/types/coordinator'
|
||||
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { generateMockPDFTest } from '@/mocks/mockTests'
|
||||
|
||||
interface PDFTestInterfaceProps {
|
||||
exam: Exam;
|
||||
studentId: string;
|
||||
onExamComplete: (session: ExamSession) => void;
|
||||
onExamSave?: (session: ExamSession) => void;
|
||||
const PDFTestInterface: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const userId = useStoreState((state) => state.auth.user.id)
|
||||
|
||||
// TODO: Replace with actual API call to fetch test by id
|
||||
const exam = generateMockPDFTest()
|
||||
|
||||
if (!exam || exam.id !== id) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Test Bulunamadı</h2>
|
||||
<p className="text-gray-600 mb-6">İstenen test bulunamadı veya erişim yetkiniz yok.</p>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Geri Dön
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
||||
exam,
|
||||
studentId,
|
||||
onExamComplete,
|
||||
onExamSave,
|
||||
}) => {
|
||||
const [session, setSession] = useState<ExamSession>({
|
||||
id: `session-${Date.now()}`,
|
||||
examId: exam.id,
|
||||
studentId,
|
||||
studentId: userId,
|
||||
startTime: new Date(),
|
||||
answers: [],
|
||||
status: "in-progress",
|
||||
status: 'in-progress',
|
||||
timeRemaining: exam.timeLimit * 60,
|
||||
});
|
||||
})
|
||||
|
||||
const [answerKeyResponses, setAnswerKeyResponses] = useState<
|
||||
Record<string, string | string[]>
|
||||
>({});
|
||||
const [answerKeyResponses, setAnswerKeyResponses] = useState<Record<string, string | string[]>>(
|
||||
{},
|
||||
)
|
||||
const [securityWarning, setSecurityWarning] = useState<{
|
||||
show: boolean;
|
||||
message: string;
|
||||
type: "warning" | "error" | "info";
|
||||
}>({ show: false, message: "", type: "warning" });
|
||||
show: boolean
|
||||
message: string
|
||||
type: 'warning' | 'error' | 'info'
|
||||
}>({ show: false, message: '', type: 'warning' })
|
||||
|
||||
// Security configuration
|
||||
const securityConfig = {
|
||||
|
|
@ -44,74 +58,86 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
disableDevTools: true,
|
||||
fullScreenMode: true,
|
||||
preventTabSwitch: true,
|
||||
};
|
||||
}
|
||||
|
||||
useExamSecurity(securityConfig, session.status === "in-progress");
|
||||
useExamSecurity(securityConfig, session.status === 'in-progress')
|
||||
|
||||
// Save test session to backend/localStorage
|
||||
const saveTestSession = (sessionData: ExamSession) => {
|
||||
// TODO: Replace with actual API call
|
||||
console.log('Saving test session:', sessionData)
|
||||
localStorage.setItem(`test-session-${sessionData.examId}`, JSON.stringify(sessionData))
|
||||
}
|
||||
|
||||
// Complete test and navigate
|
||||
const handleTestComplete = (sessionData: ExamSession) => {
|
||||
// TODO: Replace with actual API call
|
||||
console.log('Test completed:', sessionData)
|
||||
saveTestSession(sessionData)
|
||||
// Navigate to results or dashboard
|
||||
// navigate('/admin/coordinator/tests')
|
||||
}
|
||||
|
||||
// Auto-save mechanism
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (session.status === "in-progress" && onExamSave) {
|
||||
onExamSave(session);
|
||||
if (session.status === 'in-progress') {
|
||||
saveTestSession(session)
|
||||
}
|
||||
}, 30000);
|
||||
}, 30000)
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [session, onExamSave]);
|
||||
return () => clearInterval(interval)
|
||||
}, [session])
|
||||
|
||||
const handleAnswerChange = (itemId: string, answer: string | string[]) => {
|
||||
setAnswerKeyResponses((prev) => ({
|
||||
...prev,
|
||||
[itemId]: answer,
|
||||
}));
|
||||
}))
|
||||
|
||||
// Update session answers
|
||||
setSession((prev) => {
|
||||
const existingAnswerIndex = prev.answers.findIndex(
|
||||
(a) => a.questionId === itemId
|
||||
);
|
||||
const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === itemId)
|
||||
const newAnswer: StudentAnswer = {
|
||||
questionId: itemId,
|
||||
answer,
|
||||
timeSpent: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const newAnswers =
|
||||
existingAnswerIndex >= 0
|
||||
? prev.answers.map((a, i) =>
|
||||
i === existingAnswerIndex ? newAnswer : a
|
||||
)
|
||||
: [...prev.answers, newAnswer];
|
||||
? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
|
||||
: [...prev.answers, newAnswer]
|
||||
|
||||
return {
|
||||
...prev,
|
||||
answers: newAnswers,
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTimeUp = () => {
|
||||
setSecurityWarning({
|
||||
show: true,
|
||||
message: "Süre doldu! Test otomatik olarak teslim ediliyor...",
|
||||
type: "error",
|
||||
});
|
||||
message: 'Süre doldu! Test otomatik olarak teslim ediliyor...',
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
completeExam();
|
||||
}, 3000);
|
||||
};
|
||||
completeExam()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const completeExam = () => {
|
||||
const completedSession: ExamSession = {
|
||||
...session,
|
||||
endTime: new Date(),
|
||||
status: "completed",
|
||||
};
|
||||
status: 'completed',
|
||||
}
|
||||
|
||||
setSession(completedSession);
|
||||
onExamComplete(completedSession);
|
||||
};
|
||||
setSession(completedSession)
|
||||
handleTestComplete(completedSession)
|
||||
}
|
||||
|
||||
const handleSubmitExam = () => {
|
||||
const unanswered =
|
||||
|
|
@ -120,45 +146,41 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
!answerKeyResponses[item.id] ||
|
||||
(Array.isArray(answerKeyResponses[item.id]) &&
|
||||
(answerKeyResponses[item.id] as string[]).length === 0) ||
|
||||
(typeof answerKeyResponses[item.id] === "string" &&
|
||||
!answerKeyResponses[item.id])
|
||||
) || [];
|
||||
(typeof answerKeyResponses[item.id] === 'string' && !answerKeyResponses[item.id]),
|
||||
) || []
|
||||
|
||||
if (unanswered.length > 0) {
|
||||
const confirmSubmit = window.confirm(
|
||||
`${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?`
|
||||
);
|
||||
`${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?`,
|
||||
)
|
||||
|
||||
if (!confirmSubmit) return;
|
||||
if (!confirmSubmit) return
|
||||
}
|
||||
|
||||
completeExam();
|
||||
};
|
||||
completeExam()
|
||||
}
|
||||
|
||||
const getAnsweredCount = () => {
|
||||
return (
|
||||
exam.answerKeyTemplate?.filter((item) => {
|
||||
const answer = answerKeyResponses[item.id];
|
||||
const answer = answerKeyResponses[item.id]
|
||||
if (Array.isArray(answer)) {
|
||||
return answer.length > 0 && answer.some((a) => a.trim() !== "");
|
||||
return answer.length > 0 && answer.some((a) => a.trim() !== '')
|
||||
}
|
||||
return answer && answer.toString().trim() !== "";
|
||||
return answer && answer.toString().trim() !== ''
|
||||
}).length || 0
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const renderAnswerKeyItem = (item: AnswerKeyItem) => {
|
||||
const currentAnswer = answerKeyResponses[item.id];
|
||||
const currentAnswer = answerKeyResponses[item.id]
|
||||
|
||||
switch (item.type) {
|
||||
case "multiple-choice":
|
||||
case 'multiple-choice':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{item.options?.map((option, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
>
|
||||
<label key={index} className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`question-${item.id}`}
|
||||
|
|
@ -171,20 +193,20 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
|
||||
case "fill-blank":
|
||||
case 'fill-blank':
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={(currentAnswer as string) || ""}
|
||||
value={(currentAnswer as string) || ''}
|
||||
onChange={(e) => handleAnswerChange(item.id, 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"
|
||||
placeholder="Cevabınızı yazın..."
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
case "true-false":
|
||||
case 'true-false':
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
|
|
@ -192,7 +214,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
type="radio"
|
||||
name={`question-${item.id}`}
|
||||
value="true"
|
||||
checked={currentAnswer === "true"}
|
||||
checked={currentAnswer === 'true'}
|
||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
/>
|
||||
|
|
@ -203,51 +225,47 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
type="radio"
|
||||
name={`question-${item.id}`}
|
||||
value="false"
|
||||
checked={currentAnswer === "false"}
|
||||
checked={currentAnswer === 'false'}
|
||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Yanlış</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (session.status === "completed") {
|
||||
if (session.status === 'completed') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<FaCheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Test Tamamlandı!
|
||||
</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Test Tamamlandı!</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten
|
||||
sonra bilgilendirileceksiniz.
|
||||
Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
|
||||
bilgilendirileceksiniz.
|
||||
</p>
|
||||
<div className="text-sm text-gray-500">
|
||||
Başlama: {session.startTime.toLocaleString("tr-TR")}
|
||||
Başlama: {session.startTime.toLocaleString('tr-TR')}
|
||||
<br />
|
||||
Bitiş: {session.endTime?.toLocaleString("tr-TR")}
|
||||
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SecurityWarning
|
||||
isVisible={securityWarning.show}
|
||||
onDismiss={() =>
|
||||
setSecurityWarning((prev) => ({ ...prev, show: false }))
|
||||
}
|
||||
onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
|
||||
message={securityWarning.message}
|
||||
type={securityWarning.type}
|
||||
/>
|
||||
|
|
@ -256,21 +274,14 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
<header className="bg-white border-b border-gray-200 px-4 py-3 sticky top-0 z-40">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
{exam.title}
|
||||
</h1>
|
||||
<h1 className="text-xl font-semibold text-gray-900">{exam.title}</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
PDF Test - {getAnsweredCount()} /{" "}
|
||||
{exam.answerKeyTemplate?.length || 0} cevaplandı
|
||||
PDF Test - {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0} cevaplandı
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<ExamTimer
|
||||
initialTime={exam.timeLimit * 60}
|
||||
onTimeUp={handleTimeUp}
|
||||
autoStart={true}
|
||||
/>
|
||||
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
|
||||
|
||||
<button
|
||||
onClick={handleSubmitExam}
|
||||
|
|
@ -288,29 +299,21 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
{/* Document Viewer */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
{exam.testDocument?.type === "pdf" ? (
|
||||
{exam.testDocument?.type === 'pdf' ? (
|
||||
<FaFileAlt className="w-5 h-5 text-red-600" />
|
||||
) : (
|
||||
<FaImage className="w-5 h-5 text-blue-600" />
|
||||
)}
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Test Dokümanı
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500">
|
||||
({exam.testDocument?.name})
|
||||
</span>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Test Dokümanı</h3>
|
||||
<span className="text-sm text-gray-500">({exam.testDocument?.name})</span>
|
||||
</div>
|
||||
|
||||
{exam.testDocument?.type === "pdf" ? (
|
||||
{exam.testDocument?.type === 'pdf' ? (
|
||||
<div
|
||||
className="border border-gray-300 rounded-lg overflow-hidden"
|
||||
style={{ height: "600px" }}
|
||||
style={{ height: '600px' }}
|
||||
>
|
||||
<iframe
|
||||
src={exam.testDocument.url}
|
||||
className="w-full h-full"
|
||||
title="Test PDF"
|
||||
/>
|
||||
<iframe src={exam.testDocument.url} className="w-full h-full" title="Test PDF" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-gray-300 rounded-lg overflow-hidden">
|
||||
|
|
@ -325,9 +328,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
|
||||
{/* Answer Key */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Cevap Anahtarı
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Cevap Anahtarı</h3>
|
||||
|
||||
<div className="space-y-6 max-h-96 overflow-y-auto">
|
||||
{exam.answerKeyTemplate?.map((item) => {
|
||||
|
|
@ -335,15 +336,13 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
answerKeyResponses[item.id] &&
|
||||
(Array.isArray(answerKeyResponses[item.id])
|
||||
? (answerKeyResponses[item.id] as string[]).length > 0
|
||||
: answerKeyResponses[item.id].toString().trim() !== "");
|
||||
: answerKeyResponses[item.id].toString().trim() !== '')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`p-4 border-2 rounded-lg transition-all ${
|
||||
isAnswered
|
||||
? "border-green-200 bg-green-50"
|
||||
: "border-gray-200 bg-white"
|
||||
isAnswered ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
|
@ -355,14 +354,12 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
{item.points} Puan
|
||||
</span>
|
||||
</div>
|
||||
{isAnswered && (
|
||||
<FaCheckCircle className="w-5 h-5 text-green-600" />
|
||||
)}
|
||||
{isAnswered && <FaCheckCircle className="w-5 h-5 text-green-600" />}
|
||||
</div>
|
||||
|
||||
{renderAnswerKeyItem(item)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
|
@ -370,8 +367,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
<div className="mt-6 pt-4 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||
<span>
|
||||
İlerleme: {getAnsweredCount()} /{" "}
|
||||
{exam.answerKeyTemplate?.length || 0}
|
||||
İlerleme: {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0}
|
||||
</span>
|
||||
<span>Toplam Puan: {exam.totalPoints}</span>
|
||||
</div>
|
||||
|
|
@ -379,11 +375,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${
|
||||
(getAnsweredCount() /
|
||||
(exam.answerKeyTemplate?.length || 1)) *
|
||||
100
|
||||
}%`,
|
||||
width: `${(getAnsweredCount() / (exam.answerKeyTemplate?.length || 1)) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -392,5 +384,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PDFTestInterface
|
||||
Loading…
Reference in a new issue