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",
|
"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"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in a new issue