Exam ve Pdf Test Route URL

This commit is contained in:
Sedat ÖZTÜRK 2025-10-17 17:16:58 +03:00
parent cce976e158
commit 0dc6c7b521
3 changed files with 283 additions and 238 deletions

View file

@ -12644,7 +12644,7 @@
{ {
"key": "admin.coordinator.examDetail", "key": "admin.coordinator.examDetail",
"path": "/admin/coordinator/exam/:id", "path": "/admin/coordinator/exam/:id",
"componentPath": "@/views/coordinator/Exams", "componentPath": "@/views/coordinator/ExamInterface/ExamInterface",
"routeType": "protected", "routeType": "protected",
"authority": ["App.Coordinator.Exams"] "authority": ["App.Coordinator.Exams"]
}, },
@ -12658,7 +12658,7 @@
{ {
"key": "admin.coordinator.assignmentDetail", "key": "admin.coordinator.assignmentDetail",
"path": "/admin/coordinator/assignment/:id", "path": "/admin/coordinator/assignment/:id",
"componentPath": "@/views/coordinator/Assignments", "componentPath": "@/views/coordinator/ExamInterface/ExamInterface",
"routeType": "protected", "routeType": "protected",
"authority": ["App.Coordinator.Assignments"] "authority": ["App.Coordinator.Assignments"]
}, },
@ -12672,7 +12672,7 @@
{ {
"key": "admin.coordinator.testDetail", "key": "admin.coordinator.testDetail",
"path": "/admin/coordinator/test/:id", "path": "/admin/coordinator/test/:id",
"componentPath": "@/views/coordinator/Tests", "componentPath": "@/views/coordinator/ExamInterface/PDFTestInterface",
"routeType": "protected", "routeType": "protected",
"authority": ["App.Coordinator.Tests"] "authority": ["App.Coordinator.Tests"]
}, },

View file

@ -1,40 +1,54 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react'
import { QuestionRenderer } from './QuestionRenderer'; import { useParams, useNavigate } from 'react-router-dom'
import { ExamNavigation } from './ExamNavigation'; import { QuestionRenderer } from './QuestionRenderer'
import { ExamTimer } from './ExamTimer'; import { ExamNavigation } from './ExamNavigation'
import { SecurityWarning } from './SecurityWarning'; import { ExamTimer } from './ExamTimer'
import { Exam, ExamSession, StudentAnswer } from '@/types/coordinator'; import { SecurityWarning } from './SecurityWarning'
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'; import { ExamSession, StudentAnswer } from '@/types/coordinator'
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'
import { useStoreState } from '@/store/store'
import { generateMockExam } from '@/mocks/mockExams'
interface StudentExamInterfaceProps { const ExamInterface: React.FC = () => {
exam: Exam; const { id } = useParams<{ id: string }>()
studentId: string; const navigate = useNavigate()
onExamComplete: (session: ExamSession) => void; const userId = useStoreState((state) => state.auth.user.id)
onExamSave?: (session: ExamSession) => void;
}
export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({ // TODO: Replace with actual API call to fetch exam by id
exam, const exam = generateMockExam().find((e) => e.id === id)
studentId,
onExamComplete, if (!exam) {
onExamSave 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>
)
}
const [session, setSession] = useState<ExamSession>({ const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`, id: `session-${Date.now()}`,
examId: exam.id, examId: exam.id,
studentId, studentId: userId,
startTime: new Date(), startTime: new Date(),
answers: [], answers: [],
status: 'in-progress', 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<{ const [securityWarning, setSecurityWarning] = useState<{
show: boolean; show: boolean
message: string; message: string
type: 'warning' | 'error' | 'info'; type: 'warning' | 'error' | 'info'
}>({ show: false, message: '', type: 'warning' }); }>({ show: false, message: '', type: 'warning' })
// Security configuration // Security configuration
const securityConfig = { const securityConfig = {
@ -42,121 +56,150 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
disableCopyPaste: true, disableCopyPaste: true,
disableDevTools: true, disableDevTools: true,
fullScreenMode: 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 // Auto-save mechanism
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (session.status === 'in-progress' && onExamSave) { if (session.status === 'in-progress') {
onExamSave(session); saveExamSession(session)
} }
}, 30000); // Save every 30 seconds }, 30000) // Save every 30 seconds
return () => clearInterval(interval); return () => clearInterval(interval)
}, [session, onExamSave]); }, [session])
const handleAnswerChange = (questionId: string, answer: string | string[]) => { const handleAnswerChange = (questionId: string, answer: string | string[]) => {
setSession(prev => { setSession((prev) => {
const existingAnswerIndex = prev.answers.findIndex(a => a.questionId === questionId); const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === questionId)
const newAnswer: StudentAnswer = { const newAnswer: StudentAnswer = {
questionId, questionId,
answer, 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 const newAnswers =
? prev.answers.map((a, i) => i === existingAnswerIndex ? newAnswer : a) existingAnswerIndex >= 0
: [...prev.answers, newAnswer]; ? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
: [...prev.answers, newAnswer]
return { return {
...prev, ...prev,
answers: newAnswers answers: newAnswers,
}; }
}); })
}; }
const handleTimeUp = () => { const handleTimeUp = () => {
setSecurityWarning({ setSecurityWarning({
show: true, show: true,
message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...', message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...',
type: 'error' type: 'error',
}); })
setTimeout(() => { setTimeout(() => {
completeExam(); completeExam()
}, 3000); }, 3000)
}; }
const completeExam = () => { const completeExam = () => {
const completedSession: ExamSession = { const completedSession: ExamSession = {
...session, ...session,
endTime: new Date(), endTime: new Date(),
status: 'completed' status: 'completed',
}; }
setSession(completedSession); setSession(completedSession)
onExamComplete(completedSession); handleExamComplete(completedSession)
}; }
const handleSubmitExam = () => { const handleSubmitExam = () => {
const unanswered = exam.questions.filter(q => const unanswered = exam.questions.filter(
!session.answers.find(a => a.questionId === q.id && a.answer) (q) => !session.answers.find((a) => a.questionId === q.id && a.answer),
); )
if (unanswered.length > 0) { if (unanswered.length > 0) {
const confirmSubmit = window.confirm( 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 currentQuestion = exam.questions[currentQuestionIndex]
const currentAnswer = session.answers.find(a => a.questionId === currentQuestion?.id); const currentAnswer = session.answers.find((a) => a.questionId === currentQuestion?.id)
const navigateQuestion = (direction: 'next' | 'prev' | number) => { const navigateQuestion = (direction: 'next' | 'prev' | number) => {
if (typeof direction === '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') { } 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 { } else {
setCurrentQuestionIndex(prev => Math.max(0, prev - 1)); setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))
} }
}; }
if (session.status === 'completed') { if (session.status === 'completed') {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> <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="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"> <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"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 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> </svg>
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2> <h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2>
<p className="text-gray-600 mb-6"> <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> </p>
<div className="text-sm text-gray-500"> <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')} Bitiş: {session.endTime?.toLocaleString('tr-TR')}
</div> </div>
</div> </div>
</div> </div>
); )
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<SecurityWarning <SecurityWarning
isVisible={securityWarning.show} isVisible={securityWarning.show}
onDismiss={() => setSecurityWarning(prev => ({ ...prev, show: false }))} onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
message={securityWarning.message} message={securityWarning.message}
type={securityWarning.type} type={securityWarning.type}
/> />
@ -172,11 +215,7 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<ExamTimer <ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
initialTime={exam.timeLimit * 60}
onTimeUp={handleTimeUp}
autoStart={true}
/>
<button <button
onClick={handleSubmitExam} 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" 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"> <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> </svg>
<span>Önceki</span> <span>Önceki</span>
</button> </button>
@ -228,7 +272,12 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
> >
<span>Sonraki</span> <span>Sonraki</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> </svg>
</button> </button>
</div> </div>
@ -246,5 +295,7 @@ export const StudentExamInterface: React.FC<StudentExamInterfaceProps> = ({
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default ExamInterface

View file

@ -1,41 +1,55 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react'
import { ExamTimer } from "./ExamTimer"; import { useParams, useNavigate } from 'react-router-dom'
import { SecurityWarning } from "./SecurityWarning"; import { ExamTimer } from './ExamTimer'
import { FaFileAlt, FaImage, FaCheckCircle } from "react-icons/fa"; import { SecurityWarning } from './SecurityWarning'
import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from "@/types/coordinator"; import { FaFileAlt, FaImage, FaCheckCircle } from 'react-icons/fa'
import { useExamSecurity } from "@/utils/hooks/useExamSecurity"; 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 { const PDFTestInterface: React.FC = () => {
exam: Exam; const { id } = useParams<{ id: string }>()
studentId: string; const navigate = useNavigate()
onExamComplete: (session: ExamSession) => void; const userId = useStoreState((state) => state.auth.user.id)
onExamSave?: (session: ExamSession) => void;
}
export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({ // TODO: Replace with actual API call to fetch test by id
exam, const exam = generateMockPDFTest()
studentId,
onExamComplete, if (!exam || exam.id !== id) {
onExamSave, 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>
)
}
const [session, setSession] = useState<ExamSession>({ const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`, id: `session-${Date.now()}`,
examId: exam.id, examId: exam.id,
studentId, studentId: userId,
startTime: new Date(), startTime: new Date(),
answers: [], answers: [],
status: "in-progress", status: 'in-progress',
timeRemaining: exam.timeLimit * 60, timeRemaining: exam.timeLimit * 60,
}); })
const [answerKeyResponses, setAnswerKeyResponses] = useState< const [answerKeyResponses, setAnswerKeyResponses] = useState<Record<string, string | string[]>>(
Record<string, string | string[]> {},
>({}); )
const [securityWarning, setSecurityWarning] = useState<{ const [securityWarning, setSecurityWarning] = useState<{
show: boolean; show: boolean
message: string; message: string
type: "warning" | "error" | "info"; type: 'warning' | 'error' | 'info'
}>({ show: false, message: "", type: "warning" }); }>({ show: false, message: '', type: 'warning' })
// Security configuration // Security configuration
const securityConfig = { const securityConfig = {
@ -44,74 +58,86 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
disableDevTools: true, disableDevTools: true,
fullScreenMode: true, fullScreenMode: true,
preventTabSwitch: 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 // Auto-save mechanism
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (session.status === "in-progress" && onExamSave) { if (session.status === 'in-progress') {
onExamSave(session); saveTestSession(session)
} }
}, 30000); }, 30000)
return () => clearInterval(interval); return () => clearInterval(interval)
}, [session, onExamSave]); }, [session])
const handleAnswerChange = (itemId: string, answer: string | string[]) => { const handleAnswerChange = (itemId: string, answer: string | string[]) => {
setAnswerKeyResponses((prev) => ({ setAnswerKeyResponses((prev) => ({
...prev, ...prev,
[itemId]: answer, [itemId]: answer,
})); }))
// Update session answers // Update session answers
setSession((prev) => { setSession((prev) => {
const existingAnswerIndex = prev.answers.findIndex( const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === itemId)
(a) => a.questionId === itemId
);
const newAnswer: StudentAnswer = { const newAnswer: StudentAnswer = {
questionId: itemId, questionId: itemId,
answer, answer,
timeSpent: 0, timeSpent: 0,
}; }
const newAnswers = const newAnswers =
existingAnswerIndex >= 0 existingAnswerIndex >= 0
? prev.answers.map((a, i) => ? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
i === existingAnswerIndex ? newAnswer : a : [...prev.answers, newAnswer]
)
: [...prev.answers, newAnswer];
return { return {
...prev, ...prev,
answers: newAnswers, answers: newAnswers,
}; }
}); })
}; }
const handleTimeUp = () => { const handleTimeUp = () => {
setSecurityWarning({ setSecurityWarning({
show: true, show: true,
message: "Süre doldu! Test otomatik olarak teslim ediliyor...", message: 'Süre doldu! Test otomatik olarak teslim ediliyor...',
type: "error", type: 'error',
}); })
setTimeout(() => { setTimeout(() => {
completeExam(); completeExam()
}, 3000); }, 3000)
}; }
const completeExam = () => { const completeExam = () => {
const completedSession: ExamSession = { const completedSession: ExamSession = {
...session, ...session,
endTime: new Date(), endTime: new Date(),
status: "completed", status: 'completed',
}; }
setSession(completedSession); setSession(completedSession)
onExamComplete(completedSession); handleTestComplete(completedSession)
}; }
const handleSubmitExam = () => { const handleSubmitExam = () => {
const unanswered = const unanswered =
@ -120,45 +146,41 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
!answerKeyResponses[item.id] || !answerKeyResponses[item.id] ||
(Array.isArray(answerKeyResponses[item.id]) && (Array.isArray(answerKeyResponses[item.id]) &&
(answerKeyResponses[item.id] as string[]).length === 0) || (answerKeyResponses[item.id] as string[]).length === 0) ||
(typeof answerKeyResponses[item.id] === "string" && (typeof answerKeyResponses[item.id] === 'string' && !answerKeyResponses[item.id]),
!answerKeyResponses[item.id]) ) || []
) || [];
if (unanswered.length > 0) { if (unanswered.length > 0) {
const confirmSubmit = window.confirm( 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 = () => { const getAnsweredCount = () => {
return ( return (
exam.answerKeyTemplate?.filter((item) => { exam.answerKeyTemplate?.filter((item) => {
const answer = answerKeyResponses[item.id]; const answer = answerKeyResponses[item.id]
if (Array.isArray(answer)) { 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 }).length || 0
); )
}; }
const renderAnswerKeyItem = (item: AnswerKeyItem) => { const renderAnswerKeyItem = (item: AnswerKeyItem) => {
const currentAnswer = answerKeyResponses[item.id]; const currentAnswer = answerKeyResponses[item.id]
switch (item.type) { switch (item.type) {
case "multiple-choice": case 'multiple-choice':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{item.options?.map((option, index) => ( {item.options?.map((option, index) => (
<label <label key={index} className="flex items-center space-x-2 cursor-pointer">
key={index}
className="flex items-center space-x-2 cursor-pointer"
>
<input <input
type="radio" type="radio"
name={`question-${item.id}`} name={`question-${item.id}`}
@ -171,20 +193,20 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
</label> </label>
))} ))}
</div> </div>
); )
case "fill-blank": case 'fill-blank':
return ( return (
<input <input
type="text" type="text"
value={(currentAnswer as string) || ""} value={(currentAnswer as string) || ''}
onChange={(e) => handleAnswerChange(item.id, e.target.value)} 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" 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..." placeholder="Cevabınızı yazın..."
/> />
); )
case "true-false": case 'true-false':
return ( return (
<div className="flex space-x-4"> <div className="flex space-x-4">
<label className="flex items-center space-x-2 cursor-pointer"> <label className="flex items-center space-x-2 cursor-pointer">
@ -192,7 +214,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
type="radio" type="radio"
name={`question-${item.id}`} name={`question-${item.id}`}
value="true" value="true"
checked={currentAnswer === "true"} checked={currentAnswer === 'true'}
onChange={(e) => handleAnswerChange(item.id, e.target.value)} onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" 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" type="radio"
name={`question-${item.id}`} name={`question-${item.id}`}
value="false" value="false"
checked={currentAnswer === "false"} checked={currentAnswer === 'false'}
onChange={(e) => handleAnswerChange(item.id, e.target.value)} onChange={(e) => handleAnswerChange(item.id, e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/> />
<span className="text-sm text-gray-700">Yanlış</span> <span className="text-sm text-gray-700">Yanlış</span>
</label> </label>
</div> </div>
); )
default: default:
return null; return null
} }
}; }
if (session.status === "completed") { if (session.status === 'completed') {
return ( return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> <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="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"> <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" /> <FaCheckCircle className="w-8 h-8 text-green-600" />
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"> <h2 className="text-2xl font-bold text-gray-900 mb-2">Test Tamamlandı!</h2>
Test Tamamlandı!
</h2>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 mb-6">
Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
sonra bilgilendirileceksiniz. bilgilendirileceksiniz.
</p> </p>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Başlama: {session.startTime.toLocaleString("tr-TR")} Başlama: {session.startTime.toLocaleString('tr-TR')}
<br /> <br />
Bitiş: {session.endTime?.toLocaleString("tr-TR")} Bitiş: {session.endTime?.toLocaleString('tr-TR')}
</div> </div>
</div> </div>
</div> </div>
); )
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<SecurityWarning <SecurityWarning
isVisible={securityWarning.show} isVisible={securityWarning.show}
onDismiss={() => onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
setSecurityWarning((prev) => ({ ...prev, show: false }))
}
message={securityWarning.message} message={securityWarning.message}
type={securityWarning.type} 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"> <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 className="max-w-7xl mx-auto flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-semibold text-gray-900"> <h1 className="text-xl font-semibold text-gray-900">{exam.title}</h1>
{exam.title}
</h1>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
PDF Test - {getAnsweredCount()} /{" "} PDF Test - {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0} cevaplandı
{exam.answerKeyTemplate?.length || 0} cevaplandı
</p> </p>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<ExamTimer <ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
initialTime={exam.timeLimit * 60}
onTimeUp={handleTimeUp}
autoStart={true}
/>
<button <button
onClick={handleSubmitExam} onClick={handleSubmitExam}
@ -288,29 +299,21 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
{/* Document Viewer */} {/* Document Viewer */}
<div className="bg-white border border-gray-200 rounded-lg p-6"> <div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center space-x-2 mb-4"> <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" /> <FaFileAlt className="w-5 h-5 text-red-600" />
) : ( ) : (
<FaImage className="w-5 h-5 text-blue-600" /> <FaImage className="w-5 h-5 text-blue-600" />
)} )}
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">Test Dokümanı</h3>
Test Dokümanı <span className="text-sm text-gray-500">({exam.testDocument?.name})</span>
</h3>
<span className="text-sm text-gray-500">
({exam.testDocument?.name})
</span>
</div> </div>
{exam.testDocument?.type === "pdf" ? ( {exam.testDocument?.type === 'pdf' ? (
<div <div
className="border border-gray-300 rounded-lg overflow-hidden" className="border border-gray-300 rounded-lg overflow-hidden"
style={{ height: "600px" }} style={{ height: '600px' }}
> >
<iframe <iframe src={exam.testDocument.url} className="w-full h-full" title="Test PDF" />
src={exam.testDocument.url}
className="w-full h-full"
title="Test PDF"
/>
</div> </div>
) : ( ) : (
<div className="border border-gray-300 rounded-lg overflow-hidden"> <div className="border border-gray-300 rounded-lg overflow-hidden">
@ -325,9 +328,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
{/* Answer Key */} {/* Answer Key */}
<div className="bg-white border border-gray-200 rounded-lg p-6"> <div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4"> <h3 className="text-lg font-semibold text-gray-900 mb-4">Cevap Anahtarı</h3>
Cevap Anahtarı
</h3>
<div className="space-y-6 max-h-96 overflow-y-auto"> <div className="space-y-6 max-h-96 overflow-y-auto">
{exam.answerKeyTemplate?.map((item) => { {exam.answerKeyTemplate?.map((item) => {
@ -335,15 +336,13 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
answerKeyResponses[item.id] && answerKeyResponses[item.id] &&
(Array.isArray(answerKeyResponses[item.id]) (Array.isArray(answerKeyResponses[item.id])
? (answerKeyResponses[item.id] as string[]).length > 0 ? (answerKeyResponses[item.id] as string[]).length > 0
: answerKeyResponses[item.id].toString().trim() !== ""); : answerKeyResponses[item.id].toString().trim() !== '')
return ( return (
<div <div
key={item.id} key={item.id}
className={`p-4 border-2 rounded-lg transition-all ${ className={`p-4 border-2 rounded-lg transition-all ${
isAnswered isAnswered ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-white'
? "border-green-200 bg-green-50"
: "border-gray-200 bg-white"
}`} }`}
> >
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
@ -355,14 +354,12 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
{item.points} Puan {item.points} Puan
</span> </span>
</div> </div>
{isAnswered && ( {isAnswered && <FaCheckCircle className="w-5 h-5 text-green-600" />}
<FaCheckCircle className="w-5 h-5 text-green-600" />
)}
</div> </div>
{renderAnswerKeyItem(item)} {renderAnswerKeyItem(item)}
</div> </div>
); )
})} })}
</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="mt-6 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between text-sm text-gray-600"> <div className="flex items-center justify-between text-sm text-gray-600">
<span> <span>
İlerleme: {getAnsweredCount()} /{" "} İlerleme: {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0}
{exam.answerKeyTemplate?.length || 0}
</span> </span>
<span>Toplam Puan: {exam.totalPoints}</span> <span>Toplam Puan: {exam.totalPoints}</span>
</div> </div>
@ -379,11 +375,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
<div <div
className="bg-blue-600 h-2 rounded-full transition-all duration-300" className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ style={{
width: `${ width: `${(getAnsweredCount() / (exam.answerKeyTemplate?.length || 1)) * 100}%`,
(getAnsweredCount() /
(exam.answerKeyTemplate?.length || 1)) *
100
}%`,
}} }}
/> />
</div> </div>
@ -392,5 +384,7 @@ export const PDFTestInterface: React.FC<PDFTestInterfaceProps> = ({
</div> </div>
</div> </div>
</div> </div>
); )
}; }
export default PDFTestInterface