erp-platform/ui/src/views/coordinator/ExamInterface/ExamInterface.tsx

301 lines
10 KiB
TypeScript
Raw Normal View History

2025-10-17 14:16:58 +00:00
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'
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
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>
)
}
2025-10-15 19:52:01 +00:00
const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`,
examId: exam.id,
2025-10-17 14:16:58 +00:00
studentId: userId,
2025-10-15 19:52:01 +00:00
startTime: new Date(),
answers: [],
status: 'in-progress',
2025-10-17 14:16:58 +00:00
timeRemaining: exam.timeLimit * 60,
})
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
2025-10-15 19:52:01 +00:00
const [securityWarning, setSecurityWarning] = useState<{
2025-10-17 14:16:58 +00:00
show: boolean
message: string
type: 'warning' | 'error' | 'info'
}>({ show: false, message: '', type: 'warning' })
2025-10-15 19:52:01 +00:00
// Security configuration
const securityConfig = {
disableRightClick: true,
disableCopyPaste: true,
disableDevTools: true,
fullScreenMode: true,
2025-10-17 14:16:58 +00:00
preventTabSwitch: true,
}
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
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')
}
2025-10-15 19:52:01 +00:00
// Auto-save mechanism
useEffect(() => {
const interval = setInterval(() => {
2025-10-17 14:16:58 +00:00
if (session.status === 'in-progress') {
saveExamSession(session)
2025-10-15 19:52:01 +00:00
}
2025-10-17 14:16:58 +00:00
}, 30000) // Save every 30 seconds
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
return () => clearInterval(interval)
}, [session])
2025-10-15 19:52:01 +00:00
const handleAnswerChange = (questionId: string, answer: string | string[]) => {
2025-10-17 14:16:58 +00:00
setSession((prev) => {
const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === questionId)
2025-10-15 19:52:01 +00:00
const newAnswer: StudentAnswer = {
questionId,
answer,
2025-10-17 14:16:58 +00:00
timeSpent: 0, // In a real app, track time spent per question
}
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
const newAnswers =
existingAnswerIndex >= 0
? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
: [...prev.answers, newAnswer]
2025-10-15 19:52:01 +00:00
return {
...prev,
2025-10-17 14:16:58 +00:00
answers: newAnswers,
}
})
}
2025-10-15 19:52:01 +00:00
const handleTimeUp = () => {
setSecurityWarning({
show: true,
message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...',
2025-10-17 14:16:58 +00:00
type: 'error',
})
2025-10-15 19:52:01 +00:00
setTimeout(() => {
2025-10-17 14:16:58 +00:00
completeExam()
}, 3000)
}
2025-10-15 19:52:01 +00:00
const completeExam = () => {
const completedSession: ExamSession = {
...session,
endTime: new Date(),
2025-10-17 14:16:58 +00:00
status: 'completed',
}
setSession(completedSession)
handleExamComplete(completedSession)
}
2025-10-15 19:52:01 +00:00
const handleSubmitExam = () => {
2025-10-17 14:16:58 +00:00
const unanswered = exam.questions.filter(
(q) => !session.answers.find((a) => a.questionId === q.id && a.answer),
)
2025-10-15 19:52:01 +00:00
if (unanswered.length > 0) {
const confirmSubmit = window.confirm(
2025-10-17 14:16:58 +00:00
`${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`,
)
if (!confirmSubmit) return
2025-10-15 19:52:01 +00:00
}
2025-10-17 14:16:58 +00:00
completeExam()
}
2025-10-15 19:52:01 +00:00
2025-10-17 14:16:58 +00:00
const currentQuestion = exam.questions[currentQuestionIndex]
const currentAnswer = session.answers.find((a) => a.questionId === currentQuestion?.id)
2025-10-15 19:52:01 +00:00
const navigateQuestion = (direction: 'next' | 'prev' | number) => {
if (typeof direction === 'number') {
2025-10-17 14:16:58 +00:00
setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction)))
2025-10-15 19:52:01 +00:00
} else if (direction === 'next') {
2025-10-17 14:16:58 +00:00
setCurrentQuestionIndex((prev) => Math.min(exam.questions.length - 1, prev + 1))
2025-10-15 19:52:01 +00:00
} else {
2025-10-17 14:16:58 +00:00
setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))
2025-10-15 19:52:01 +00:00
}
2025-10-17 14:16:58 +00:00
}
2025-10-15 19:52:01 +00:00
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">
2025-10-17 14:16:58 +00:00
<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"
/>
2025-10-15 19:52:01 +00:00
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2>
<p className="text-gray-600 mb-6">
2025-10-17 14:16:58 +00:00
Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
bilgilendirileceksiniz.
2025-10-15 19:52:01 +00:00
</p>
<div className="text-sm text-gray-500">
2025-10-17 14:16:58 +00:00
Başlama: {session.startTime.toLocaleString('tr-TR')}
<br />
2025-10-15 19:52:01 +00:00
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
</div>
</div>
</div>
2025-10-17 14:16:58 +00:00
)
2025-10-15 19:52:01 +00:00
}
return (
<div className="min-h-screen bg-gray-50">
<SecurityWarning
isVisible={securityWarning.show}
2025-10-17 14:16:58 +00:00
onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
2025-10-15 19:52:01 +00:00
message={securityWarning.message}
type={securityWarning.type}
/>
{/* Header */}
<header className="bg-white border-b border-gray-200 px-4 py-3">
<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>
<p className="text-sm text-gray-600">
Soru {currentQuestionIndex + 1} / {exam.questions.length}
</p>
</div>
2025-10-17 14:16:58 +00:00
2025-10-15 19:52:01 +00:00
<div className="flex items-center space-x-4">
2025-10-17 14:16:58 +00:00
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
2025-10-15 19:52:01 +00:00
<button
onClick={handleSubmitExam}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
>
Sınavı Teslim Et
</button>
</div>
</div>
</header>
{/* Main Content */}
<div className="max-w-7xl mx-auto p-4">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Question Content */}
<div className="lg:col-span-3 space-y-6">
{currentQuestion && (
<QuestionRenderer
question={currentQuestion}
answer={currentAnswer}
onAnswerChange={handleAnswerChange}
disabled={session.status !== 'in-progress'}
/>
)}
{/* Navigation Controls */}
<div className="flex items-center justify-between bg-white border border-gray-200 rounded-lg p-4">
<button
onClick={() => navigateQuestion('prev')}
disabled={currentQuestionIndex === 0}
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">
2025-10-17 14:16:58 +00:00
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
2025-10-15 19:52:01 +00:00
</svg>
<span>Önceki</span>
</button>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">
{currentQuestionIndex + 1} / {exam.questions.length}
</span>
</div>
<button
onClick={() => navigateQuestion('next')}
disabled={currentQuestionIndex === exam.questions.length - 1}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-50 disabled:text-gray-400 text-white rounded-lg font-medium transition-colors"
>
<span>Sonraki</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2025-10-17 14:16:58 +00:00
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
2025-10-15 19:52:01 +00:00
</svg>
</button>
</div>
</div>
{/* Navigation Sidebar */}
<div className="lg:col-span-1">
<ExamNavigation
questions={exam.questions}
answers={session.answers}
currentQuestionIndex={currentQuestionIndex}
onQuestionSelect={navigateQuestion}
/>
</div>
</div>
</div>
</div>
2025-10-17 14:16:58 +00:00
)
}
export default ExamInterface