Initial addition of the Arcade study feature. Add connections as the first working game.
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m4s

This commit is contained in:
Elijah 2026-07-13 17:35:18 -07:00
parent 7b90409f2e
commit 611a585757
43 changed files with 5990 additions and 68 deletions

View file

@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
import { ArcadeImportError } from "@/lib/arcade/connectionsImport";
import { arcadePreviewRequestSchema } from "@/lib/validation/arcadeSchemas";
import * as arcadeService from "@/services/arcadeService";
export async function POST(request: NextRequest) {
const parsed = arcadePreviewRequestSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ error: "A game type and raw JSON are required." }, { status: 400 });
}
try {
return NextResponse.json(arcadeService.previewArcadeImport(parsed.data.rawJson));
} catch (error) {
if (error instanceof ArcadeImportError) {
return NextResponse.json({ error: error.message, details: error.details }, { status: 400 });
}
throw error;
}
}

View file

@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { arcadeAttemptCreateSchema } from "@/lib/validation/arcadeSchemas";
import * as arcadeService from "@/services/arcadeService";
export async function GET(
_request: NextRequest,
context: RouteContext<"/api/arcade/packs/[id]/attempts">
) {
const { id } = await context.params;
const pack = await arcadeService.getArcadePack(id);
if (!pack) return NextResponse.json({ error: "Arcade pack not found" }, { status: 404 });
return NextResponse.json(await arcadeService.listArcadeAttempts(id));
}
export async function POST(
request: NextRequest,
context: RouteContext<"/api/arcade/packs/[id]/attempts">
) {
const { id } = await context.params;
const parsed = arcadeAttemptCreateSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ error: "Invalid completed attempt", details: parsed.error.issues }, { status: 400 });
}
try {
const attempt = await arcadeService.createArcadeAttempt(id, parsed.data);
return attempt
? NextResponse.json(attempt, { status: 201 })
: NextResponse.json({ error: "Arcade pack not found" }, { status: 404 });
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Attempt could not be saved" },
{ status: 400 }
);
}
}

View file

@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from "next/server";
import { arcadePackUpdateSchema } from "@/lib/validation/arcadeSchemas";
import * as arcadeService from "@/services/arcadeService";
export async function GET(
_request: NextRequest,
context: RouteContext<"/api/arcade/packs/[id]">
) {
const { id } = await context.params;
const pack = await arcadeService.getArcadePack(id);
return pack
? NextResponse.json(pack)
: NextResponse.json({ error: "Arcade pack not found" }, { status: 404 });
}
export async function PATCH(
request: NextRequest,
context: RouteContext<"/api/arcade/packs/[id]">
) {
const { id } = await context.params;
const parsed = arcadePackUpdateSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) return NextResponse.json({ error: "A valid name is required" }, { status: 400 });
const pack = await arcadeService.updateArcadePack(id, parsed.data.name);
return pack
? NextResponse.json(pack)
: NextResponse.json({ error: "Arcade pack not found" }, { status: 404 });
}
export async function DELETE(
_request: NextRequest,
context: RouteContext<"/api/arcade/packs/[id]">
) {
const { id } = await context.params;
const deleted = await arcadeService.deleteArcadePack(id);
return deleted
? NextResponse.json({ success: true })
: NextResponse.json({ error: "Arcade pack not found" }, { status: 404 });
}

View file

@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { ArcadeImportError } from "@/lib/arcade/connectionsImport";
import { arcadePackCreateSchema } from "@/lib/validation/arcadeSchemas";
import * as arcadeService from "@/services/arcadeService";
export async function GET(request: NextRequest) {
const params = new URL(request.url).searchParams;
const classId = params.get("classId");
const gameType = params.get("gameType");
if (!classId || gameType !== "connections") {
return NextResponse.json({ error: "Valid classId and gameType values are required." }, { status: 400 });
}
return NextResponse.json(await arcadeService.listArcadePacks(classId, gameType));
}
export async function POST(request: NextRequest) {
const parsed = arcadePackCreateSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ error: "Invalid Arcade pack request", details: parsed.error.issues }, { status: 400 });
}
try {
const packs = await arcadeService.createArcadePacks(parsed.data);
return NextResponse.json({ packs, count: packs.length }, { status: 201 });
} catch (error) {
if (error instanceof ArcadeImportError) {
return NextResponse.json({ error: error.message, details: error.details }, { status: 400 });
}
if (error instanceof Error) {
const status = error.message === "Class not found" ? 404 : 400;
return NextResponse.json({ error: error.message }, { status });
}
throw error;
}
}

View file

@ -1,16 +1,21 @@
import { NextRequest, NextResponse } from "next/server";
import * as settingsService from "@/services/settingsService";
function getInstructionType(request: NextRequest): settingsService.LlmInstructionType | null {
const type = new URL(request.url).searchParams.get("type") ?? "flashcards";
return type === "flashcards" || type === "quizzes" || type === "connections" ? type : null;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") as "flashcards" | "quizzes" || "flashcards";
const type = getInstructionType(request);
if (!type) return NextResponse.json({ error: "Invalid instruction type" }, { status: 400 });
const instructions = await settingsService.getLlmInstructions(type);
return NextResponse.json({ value: instructions });
}
export async function PATCH(request: NextRequest) {
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") as "flashcards" | "quizzes" || "flashcards";
const type = getInstructionType(request);
if (!type) return NextResponse.json({ error: "Invalid instruction type" }, { status: 400 });
const body = await request.json().catch(() => null);
if (!body || typeof body.value !== "string") {