TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Révision d'une carte : SM-2 + événement de maîtrise + journal.2import { NextResponse } from "next/server";3import { z } from "zod";4import { apiError, parseBody } from "@/lib/api.ts";5import { assertSameOrigin, requireUser } from "@/lib/auth/session.ts";6import { get, run } from "@/lib/db/index.ts";7import { initialCardState, reviewCard, type ReviewQuality } from "@/lib/learning/sm2.ts";8import { recordMasteryEvent } from "@/lib/learning/mastery.ts";9import { logActivity } from "@/lib/usage.ts";1011const schema = z.object({12 cardId: z.number().int().positive(),13 q: z.union([z.literal(2), z.literal(3), z.literal(4), z.literal(5)]),14 suspend: z.boolean().optional(),15 favorite: z.boolean().optional(),16});1718export async function POST(req: Request) {19 try {20 await assertSameOrigin();21 const user = await requireUser();22 const body = await parseBody(req, schema);23 const card = get<{ id: number; course_code: string; concept_id: number | null; owner_user_id: number | null }>(24 "SELECT id, course_code, concept_id, owner_user_id FROM flashcards WHERE id = ?", body.cardId25 );26 if (!card || (card.owner_user_id && card.owner_user_id !== user.id)) {27 return NextResponse.json({ error: "Carte introuvable." }, { status: 404 });28 }29 const enrolled = get("SELECT 1 as ok FROM enrollments WHERE user_id = ? AND course_code = ?", user.id, card.course_code);30 if (!enrolled) return NextResponse.json({ error: "Accès refusé." }, { status: 403 });3132 const state = get<{ ef: number; interval_days: number; reps: number; lapses: number }>(33 "SELECT ef, interval_days, reps, lapses FROM card_states WHERE user_id = ? AND card_id = ?", user.id, body.cardId34 );35 const prev = state36 ? { ef: state.ef, intervalDays: state.interval_days, reps: state.reps, lapses: state.lapses }37 : initialCardState();38 const next = reviewCard(prev, body.q as ReviewQuality);39 const dueExpr = next.dueInDays === 0 ? "datetime('now', '+10 minutes')" : `datetime('now', '+${next.dueInDays} days')`;4041 run(42 `INSERT INTO card_states (user_id, card_id, ef, interval_days, reps, lapses, due_at, suspended, favorite, last_reviewed_at)43 VALUES (?, ?, ?, ?, ?, ?, ${dueExpr}, ?, ?, datetime('now'))44 ON CONFLICT(user_id, card_id) DO UPDATE SET ef = excluded.ef, interval_days = excluded.interval_days,45 reps = excluded.reps, lapses = excluded.lapses, due_at = excluded.due_at,46 suspended = excluded.suspended, favorite = excluded.favorite, last_reviewed_at = excluded.last_reviewed_at`,47 user.id, body.cardId, next.ef, next.intervalDays, next.reps, next.lapses,48 body.suspend ? 1 : 0, body.favorite ? 1 : 049 );50 run(51 "INSERT INTO review_log (user_id, card_id, q, interval_before) VALUES (?, ?, ?, ?)",52 user.id, body.cardId, body.q, prev.intervalDays53 );54 if (card.concept_id) {55 recordMasteryEvent({56 userId: user.id, conceptId: card.concept_id, kind: "flashcard",57 correct: body.q >= 4, difficulty: body.q === 3 ? 3 : 2,58 });59 }60 logActivity(user.id, "flashcards", card.course_code, 20);61 return NextResponse.json({ ok: true, nextDueInDays: next.dueInDays, ef: next.ef });62 } catch (e) {63 return apiError(e);64 }65}66