import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { QRCodeSVG } from 'qrcode.react'; import './styles.css'; const api = async (path, options = {}) => { const response = await fetch(`/api${path}`, { credentials: 'include', headers: { Accept: 'application/json', ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...options.headers }, ...options }); const payload = await response.json(); if (!response.ok || !payload.success) throw new Error(payload.error?.message || 'Request failed'); return payload.data; }; function useSession() { const [session, setSession] = useState(null); useEffect(() => { api('/auth/session').then(setSession).catch(() => setSession({ user: null })); }, []); return [session, setSession]; } function Login({ onLogin }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const login = async e => { e.preventDefault(); try { const data = await api('/auth/login', { method: 'POST', headers: { 'X-CSRF-Token': (await api('/auth/session')).csrfToken }, body: JSON.stringify({ email, password }) }); onLogin(data); } catch (err) { setError(err.message); } }; return

ExamFlow Control

Sign in to manage classroom sessions.

setEmail(e.target.value)} required/> setPassword(e.target.value)} required/>{error &&

{error}

}
; } function Admin() { const [auth, setAuth] = useSession(); const [sessions, setSessions] = useState([]); const [exams, setExams] = useState([]); const [rooms, setRooms] = useState([]); const [message, setMessage] = useState(''); const load = async () => { try { const token = auth?.csrfToken || (await api('/auth/session')).csrfToken; const options = { method: 'POST', headers: { 'X-CSRF-Token': token }, body: JSON.stringify({ action: 'select', limit: 500 }) }; const [s, e, r] = await Promise.all([api('/admin/exam-sessions'), api('/admin/data/exams', options), api('/admin/data/organization_units', options)]); setSessions(s); setExams(e.rows.filter(x => ['published', 'active'].includes(x.status))); setRooms(r.rows.filter(x => x.unit_type === 'classroom')); } catch (err) { setMessage(err.message); } }; useEffect(() => { if (auth?.user) load(); }, [auth?.user]); if (!auth) return
Loading…
; if (!auth.user) return ; const create = async e => { e.preventDefault(); const f = new FormData(e.target); try { await api('/admin/exam-sessions', { method: 'POST', headers: { 'X-CSRF-Token': auth.csrfToken }, body: JSON.stringify(Object.fromEntries(f)) }); e.target.reset(); setMessage('Classroom QR session created.'); load(); } catch (err) { setMessage(err.message); } }; const control = async (id, action) => { try { await api(`/admin/exam-sessions/${id}/control`, { method: 'POST', headers: { 'X-CSRF-Token': auth.csrfToken }, body: JSON.stringify({ action }) }); load(); } catch (err) { setMessage(err.message); } }; return
ExamFlowClassroom session control

Create classroom session

Each session gets a distinct QR, live board, and result reveal.

{message &&

{message}

}

Sessions

{sessions.map(s => )}
; } function SessionCard({ session, control }) { const root = window.location.origin; const student = `${root}/session/${session.public_token}`; const board = `${root}/live/${session.public_token}`; return
{session.status.replace('_', ' ')}

{session.name}

{session.exam_name} · {session.classroom_name}

Open student QR linkOpen live display
{session.status === 'scheduled' && }{session.status === 'live' && }{session.status === 'closed' && }{session.status === 'results_revealed' && }
; } function Student({ token }) { const [session, setSession] = useState(null); const [registration, setRegistration] = useState(null); const [otp, setOtp] = useState(''); const [notice, setNotice] = useState(''); useEffect(() => { api(`/public/exam-sessions/${token}`).then(setSession).catch(e => setNotice(e.message)); }, [token]); const enroll = async e => { e.preventDefault(); try { const csrfToken = (await api('/auth/session')).csrfToken; const data = await api(`/public/exam-sessions/${token}/registrations`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: JSON.stringify(Object.fromEntries(new FormData(e.target))) }); setRegistration(data); setNotice('OTP sent to your mobile.'); } catch (err) { setNotice(err.message); } }; const verify = async e => { e.preventDefault(); try { const csrfToken = (await api('/auth/session')).csrfToken; await api(`/public/registrations/${registration.id}/otp/verify`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ otp }) }); const attempt = await api(`/public/registrations/${registration.id}/attempts`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: JSON.stringify({}) }); window.location.assign(`/exam/take/${attempt.id}`); } catch (err) { setNotice(err.message); } }; if (!session) return

{notice || 'Loading classroom exam…'}

; return

{session.classroom_name}

{session.exam_name}

{session.duration_minutes} minutes · {session.total_marks} marks

{!registration ?
:
setOtp(e.target.value)} placeholder="6-digit OTP" inputMode="numeric" required/>
}{notice &&

{notice}

}
; } function LiveBoard({ token }) { const [data, setData] = useState(null); const [error, setError] = useState(''); useEffect(() => { const load = () => api(`/public/exam-sessions/${token}/live-board`).then(setData).catch(e => setError(e.message)); load(); const timer = setInterval(load, 5000); return () => clearInterval(timer); }, [token]); if (!data) return

{error || 'Loading live results…'}

; const medals = ['🥇', '🥈', '🥉']; return

{data.session.classroom_name}

{data.session.exam_name}

{Object.entries(data.stats).map(([k,v]) =>
{v}{k.replace('_', ' ')}
)}
{data.results_revealed ?

Classroom champions

{data.top_10.slice(0,3).map((x,i) =>
{medals[i]} #{i + 1}

{x.name}

{x.percentage}% · {x.score} marks

)}

Top 10

    {data.top_10.map(x =>
  1. {x.name}{x.percentage}%
  2. )}
:

Results will be revealed shortly

The live display updates every 5 seconds.

}
; } function TakeExam({ attemptId }) { const [data, setData] = useState(null); const [answers, setAnswers] = useState({}); const [error, setError] = useState(''); const [submitting, setSubmitting] = useState(false); useEffect(() => { api(`/public/attempts/${attemptId}`).then(setData).catch(e => setError(e.message)); }, [attemptId]); const submit = async () => { if (!data || submitting) return; setSubmitting(true); try { const csrfToken = (await api('/auth/session')).csrfToken; const rows = Object.entries(answers).map(([question_id, selected_answer]) => ({ question_id, selected_answer })); await api(`/public/attempts/${attemptId}/answers`, { method: 'PUT', headers: { 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ answers: rows }) }); await api(`/public/attempts/${attemptId}/submit`, { method: 'POST', headers: { 'X-CSRF-Token': csrfToken }, body: JSON.stringify({}) }); window.location.assign(`/exam/result/${attemptId}`); } catch (e) { setError(e.message); setSubmitting(false); } }; if (!data) return

{error || 'Loading exam...'}

; return
{data.exam.name}{data.exam.duration_minutes} minutes · {data.questions.length} questions
{data.questions.map((q, index) =>

Question {index + 1} · {q.marks} marks

{q.question_text}

{q.question_type === 'numerical' ? setAnswers({ ...answers, [q.id]: e.target.value })}/> :
{q.options.map(option => )}
}
)}{error &&

{error}

}
; } function ExamResult({ attemptId }) { const [result, setResult] = useState(null); const [error, setError] = useState(''); useEffect(() => { api(`/public/attempts/${attemptId}/result`).then(setResult).catch(e => setError(e.message)); }, [attemptId]); if (!result) return

{error || 'Calculating result...'}

; return

{result.exam.name}

{result.attempt.passed ? 'Congratulations!' : 'Exam completed'}

{result.attempt.percentage}%

{result.attempt.score} marks · Rank #{result.attempt.rank || '—'}

{result.attempt.correct_count} correct · {result.attempt.wrong_count} incorrect · {result.attempt.skipped_count} skipped

; } const path = window.location.pathname.split('/').filter(Boolean); const screen = path[0] === 'session' ? : path[0] === 'live' ? : path[0] === 'exam' && path[1] === 'take' ? : path[0] === 'exam' && path[1] === 'result' ? : ; createRoot(document.getElementById('root')).render(screen);