Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions frontend/src/features/jobs/PostJobPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [expired, setExpired] = useState(() => new Set());
// Index of the question currently on screen. Only this one is mounted, so only
// its countdown runs; advancing remounts the next card with a fresh clock.
const [currentIndex, setCurrentIndex] = useState(0);

// Live countdown per question, kept in a ref so ticking never re-renders the page.
const timeLeft = useRef({});
Expand All @@ -35,11 +38,31 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
const sent = useRef({ answers: false, followup: false });

const limit = quiz?.time_limit_seconds ?? 75;
const questions = quiz?.questions ?? [];
const currentQuestion = questions[currentIndex] ?? null;
const isLastQuestion = currentIndex >= questions.length - 1;

function markExpired(id) {
setExpired((prev) => (prev.has(id) ? prev : new Set(prev).add(id)));
}

// Moving on forfeits whatever time is left on the current question; the clock is
// per question, so there is no going back to spend it later.
function advance() {
if (isLastQuestion) {
handleSubmit();
} else {
setCurrentIndex(currentIndex + 1);
}
}

// Expiry locks the answer as-is and moves straight on, so the poster never sits
// on a dead card. Running out on the last question commits the round.
function handleExpire(id) {
markExpired(id);
advance();
}

function reset() {
setQuiz(null);
setAnswers({});
Expand All @@ -48,6 +71,7 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
setResult(null);
setError("");
setExpired(new Set());
setCurrentIndex(0);
timeLeft.current = {};
inputSignal.current = {};
sent.current = { answers: false, followup: false };
Expand Down Expand Up @@ -122,13 +146,6 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
}
}

// When every question's clock has run out, submit whatever is there.
useEffect(() => {
if (quiz && !followup && !result && expired.size >= quiz.questions.length) {
handleSubmit();
}
}, [expired, quiz, followup, result]);

const followupExpired = followup && expired.has(followup.followup.id);
useEffect(() => {
if (followupExpired && !result) handleFollowUp();
Expand Down Expand Up @@ -180,18 +197,20 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
</div>

<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{quiz.questions.map((q) => (
{currentQuestion && (
<QuestionCard
key={q.id}
question={q}
answer={answers[q.id] || ""}
key={currentQuestion.id}
question={currentQuestion}
questionNumber={currentIndex + 1}
totalQuestions={questions.length}
answer={answers[currentQuestion.id] || ""}
timeLimit={limit}
onTick={(id, s) => (timeLeft.current[id] = s)}
onExpire={markExpired}
onExpire={handleExpire}
onInputSignal={(id, sig) => (inputSignal.current[id] = sig)}
onAnswerChange={(id, val) => setAnswers((prev) => ({ ...prev, [id]: val }))}
/>
))}
)}
</div>

<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px", marginTop: "6px" }}>
Expand All @@ -200,8 +219,12 @@ export default function PostJobPage({ onUnauthorized, onCancel, onViewJobs }) {
Cancel
</button>
)}
<button className="btn btn-primary" onClick={handleSubmit} disabled={loading}>
{loading ? "Submitting for jury review..." : "Submit Answers & Open Follow-up"}
<button className="btn btn-primary" onClick={advance} disabled={loading}>
{loading
? "Submitting for jury review..."
: isLastQuestion
? "Submit Answers & Open Follow-up"
: "Next Question"}
</button>
</div>
</div>
Expand Down
56 changes: 40 additions & 16 deletions frontend/src/features/quiz/QuizPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export default function QuizPage({
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [expired, setExpired] = useState(() => new Set());
// Index of the question currently on screen. Only this one is mounted, so only
// its countdown runs; advancing remounts the next card with a fresh clock.
const [currentIndex, setCurrentIndex] = useState(0);

// Live countdown per question, kept in a ref so ticking never re-renders the page.
const timeLeft = useRef({});
Expand All @@ -36,11 +39,31 @@ export default function QuizPage({
const sent = useRef({ answers: false, followup: false });

const limit = quiz?.time_limit_seconds ?? 75;
const questions = quiz?.questions ?? [];
const currentQuestion = questions[currentIndex] ?? null;
const isLastQuestion = currentIndex >= questions.length - 1;

function markExpired(id) {
setExpired((prev) => (prev.has(id) ? prev : new Set(prev).add(id)));
}

// Moving on forfeits whatever time is left on the current question; the clock is
// per question, so there is no going back to spend it later.
function advance() {
if (isLastQuestion) {
handleSubmit();
} else {
setCurrentIndex(currentIndex + 1);
}
}

// Expiry locks the answer as-is and moves straight on, so the candidate never
// sits on a dead card. Running out on the last question commits the round.
function handleExpire(id) {
markExpired(id);
advance();
}

function handleReset() {
setQuiz(null);
setAnswers({});
Expand All @@ -49,6 +72,7 @@ export default function QuizPage({
setResult(null);
setError("");
setExpired(new Set());
setCurrentIndex(0);
timeLeft.current = {};
inputSignal.current = {};
sent.current = { answers: false, followup: false };
Expand All @@ -63,6 +87,7 @@ export default function QuizPage({
setFollowup(null);
setFollowupAnswer("");
setExpired(new Set());
setCurrentIndex(0);
timeLeft.current = {};
inputSignal.current = {};
sent.current = { answers: false, followup: false };
Expand Down Expand Up @@ -94,6 +119,7 @@ export default function QuizPage({
setFollowup(null);
setFollowupAnswer("");
setExpired(new Set());
setCurrentIndex(0);
timeLeft.current = {};
inputSignal.current = {};
sent.current = { answers: false, followup: false };
Expand Down Expand Up @@ -155,12 +181,6 @@ export default function QuizPage({
}
}

useEffect(() => {
if (quiz && !followup && !result && expired.size >= quiz.questions.length) {
handleSubmit();
}
}, [expired, quiz, followup, result]);

const followupExpired = followup && expired.has(followup.followup.id);
useEffect(() => {
if (followupExpired && !result) handleFollowUp();
Expand Down Expand Up @@ -270,28 +290,32 @@ export default function QuizPage({
</div>
</div>

{quiz.questions.map((q, idx) => (
{currentQuestion && (
<QuestionCard
key={q.id}
question={q}
questionNumber={idx + 1}
totalQuestions={quiz.questions.length}
answer={answers[q.id]}
key={currentQuestion.id}
question={currentQuestion}
questionNumber={currentIndex + 1}
totalQuestions={questions.length}
answer={answers[currentQuestion.id]}
timeLimit={limit}
onTick={(id, s) => (timeLeft.current[id] = s)}
onExpire={markExpired}
onExpire={handleExpire}
onInputSignal={(id, sig) => (inputSignal.current[id] = sig)}
onAnswerChange={(id, val) => setAnswers((prev) => ({ ...prev, [id]: val }))}
/>
))}
)}

<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "8px" }}>
<button
className="btn btn-primary btn-lg"
onClick={handleSubmit}
onClick={advance}
disabled={loading}
>
{loading ? "Submitting Answers..." : "Submit Round for Defense"}
{loading
? "Submitting Answers..."
: isLastQuestion
? "Submit Round for Defense"
: "Next Question"}
</button>
</div>
</div>
Expand Down
Loading