erp-platform/ui/src/views/coordinator/ExamInterface/ExamInterface.tsx
2025-10-17 17:16:58 +03:00

301 lines
No EOL
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
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>
)
}
const [session, setSession] = useState<ExamSession>({
id: `session-${Date.now()}`,
examId: exam.id,
studentId: userId,
startTime: new Date(),
answers: [],
status: 'in-progress',
timeRemaining: exam.timeLimit * 60,
})
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
const [securityWarning, setSecurityWarning] = useState<{
show: boolean
message: string
type: 'warning' | 'error' | 'info'
}>({ show: false, message: '', type: 'warning' })
// Security configuration
const securityConfig = {
disableRightClick: true,
disableCopyPaste: true,
disableDevTools: true,
fullScreenMode: true,
preventTabSwitch: true,
}
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') {
saveExamSession(session)
}
}, 30000) // Save every 30 seconds
return () => clearInterval(interval)
}, [session])
const handleAnswerChange = (questionId: string, answer: string | string[]) => {
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
}
const newAnswers =
existingAnswerIndex >= 0
? 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! Sınav otomatik olarak teslim ediliyor...',
type: 'error',
})
setTimeout(() => {
completeExam()
}, 3000)
}
const completeExam = () => {
const completedSession: ExamSession = {
...session,
endTime: new Date(),
status: 'completed',
}
setSession(completedSession)
handleExamComplete(completedSession)
}
const handleSubmitExam = () => {
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?`,
)
if (!confirmSubmit) return
}
completeExam()
}
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)))
} else if (direction === 'next') {
setCurrentQuestionIndex((prev) => Math.min(exam.questions.length - 1, prev + 1))
} else {
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>
</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.
</p>
<div className="text-sm text-gray-500">
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 }))}
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>
<div className="flex items-center space-x-4">
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
<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">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</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">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</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>
)
}
export default ExamInterface