Refactor Study Desk application structure

This commit is contained in:
Elijah 2026-08-07 19:31:23 -07:00
parent faaccf8a7e
commit 089439ed90
145 changed files with 8087 additions and 3412 deletions

View file

@ -1,65 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { getQuizSetWithQuestions, createQuizAttempt, listQuizAttempts } from "@/services/quizService";
import { scoreQuiz } from "@/lib/scoring";
import {
QuizAttemptValidationError,
QuizNotFoundError,
listQuizAttempts,
submitQuizAttempt,
} from "@/services/quizService";
import { quizAttemptSchema } from "@/lib/validation/attemptSchemas";
export async function POST(
request: NextRequest,
ctx: RouteContext<"/api/quizzes/[id]/attempt">
) {
const { id } = await ctx.params;
const body = await request.json().catch(() => null);
if (!body || !body.answersJson) {
const parsed = quizAttemptSchema.safeParse(
await request.json().catch(() => null)
);
if (!parsed.success) {
return NextResponse.json(
{ error: "answersJson is required" },
{ error: parsed.error.issues[0]?.message ?? "Invalid attempt" },
{ status: 400 }
);
}
const quizSet = await getQuizSetWithQuestions(id);
if (!quizSet) {
return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
}
// Parse answers
let answers: Record<string, string[]>;
try {
answers = JSON.parse(body.answersJson);
} catch {
return NextResponse.json({ error: "Invalid answers JSON" }, { status: 400 });
return NextResponse.json(await submitQuizAttempt(id, parsed.data), {
status: 201,
});
} catch (error) {
if (error instanceof QuizNotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}
if (error instanceof QuizAttemptValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
console.error("Failed to submit quiz attempt", error);
return NextResponse.json({ error: "Failed to submit attempt" }, { status: 500 });
}
// Determine which questions were included in this attempt
const isPartialRetake = body.isPartialRetake === true;
// If partial retake, we only score the questions that actually had answers provided
// or that were explicitly passed in a questionIds array.
// For simplicity, we filter the quizSet questions down to what's in the answers object
// if it's a partial retake, though the client will only show those anyway.
let questionsToScore = quizSet.questions;
if (isPartialRetake) {
const answeredIds = Object.keys(answers);
questionsToScore = quizSet.questions.filter(q => answeredIds.includes(q.id));
}
// Format questions for the scoring utility
const formattedQuestions = questionsToScore.map((q) => ({
id: q.id,
type: q.type as "MULTIPLE_CHOICE" | "SATA",
options: q.options.map((o) => ({ id: o.id, isCorrect: o.isCorrect })),
}));
const { total, maxScore } = scoreQuiz(formattedQuestions, answers);
const attempt = await createQuizAttempt({
quizSetId: id,
score: total,
maxScore: maxScore,
answersJson: body.answersJson,
isPartialRetake,
});
return NextResponse.json(attempt, { status: 201 });
}
export async function GET(