diff --git a/apps/api/package.json b/apps/api/package.json index 8aeb33f..9319bb7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -19,6 +19,7 @@ "@prisma/client": "6.11.1", "@tiptap/extension-horizontal-rule": "2.1.13", "adminjs": "^7.8.17", + "axios": "^1.11.0", "better-auth": "^1.2.12", "cors": "^2.8.5", "dotenv": "^17.2.1", diff --git a/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql b/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql new file mode 100644 index 0000000..4c2d110 --- /dev/null +++ b/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,createdAt]` on the table `DailyQuiz` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "DailyQuiz" ADD COLUMN "totalQuestions" INTEGER NOT NULL DEFAULT 10; + +-- AlterTable +ALTER TABLE "Topic" ADD COLUMN "attempted" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "difficulty" "QuestionDifficulty" NOT NULL DEFAULT 'easy', +ADD COLUMN "solved" INTEGER NOT NULL DEFAULT 0; + +-- CreateIndex +CREATE UNIQUE INDEX "DailyQuiz_userId_createdAt_key" ON "DailyQuiz"("userId", "createdAt"); diff --git a/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql b/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql new file mode 100644 index 0000000..0171224 --- /dev/null +++ b/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,quizDate]` on the table `DailyQuiz` will be added. If there are existing duplicate values, this will fail. + - Added the required column `quizDate` to the `DailyQuiz` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "DailyQuiz_userId_createdAt_key"; + +-- AlterTable +ALTER TABLE "DailyQuiz" ADD COLUMN "quizDate" TIMESTAMP(3) NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "DailyQuiz_userId_quizDate_key" ON "DailyQuiz"("userId", "quizDate"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 14c4e60..f2bee33 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -67,6 +67,12 @@ model Topic { userCompletions UserCompletion[] questions Question[] userPerformances UserTopicPerformance[] + + + difficulty QuestionDifficulty @default(easy) + attempted Int @default(0) + solved Int @default(0) + notes Note[] } @@ -85,6 +91,7 @@ model Note { topic Topic @relation(fields: [topicId], references: [id], onDelete: Cascade) courseId String course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + } model UserCompletion { @@ -153,6 +160,10 @@ model DailyQuiz { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt submittedAt DateTime? + totalQuestions Int @default(10) + quizDate DateTime // Should be set to the date (midnight) of the quiz, without time component + + @@unique([userId, quizDate]) } model User { @@ -246,3 +257,4 @@ model Jwks { @@map("jwks") } + diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 900a5ad..4a6274d 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -3,6 +3,7 @@ import express from "express"; import { auth } from "./lib/auth.js"; import { toNodeHandler } from "better-auth/node"; import { admin, adminRouter } from "./lib/admin.js"; +import { quizzesRouter } from "./routes/quizzes.js"; import api from "./routes/index.js"; import { notesRouter } from "./routes/notes.js"; @@ -12,8 +13,12 @@ app.disable("x-powered-by"); app.all("/api/auth/*", toNodeHandler(auth)); app.use(admin.options.rootPath, adminRouter); console.log(`AdminJS is running under ${admin.options.rootPath}`); + app.use(express.json()); + app.use(notesRouter); app.use("/api", api); +app.use("/api/quizzes", quizzesRouter); + export default app; diff --git a/apps/api/src/controller/calendar.controller.ts b/apps/api/src/controller/calendar.controller.ts new file mode 100644 index 0000000..d7b2d15 --- /dev/null +++ b/apps/api/src/controller/calendar.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response } from "express"; +import type { Session, User } from "better-auth"; +import { prisma } from "../lib/prisma.js"; + +declare global { + // Ensure session typing just like other controllers + namespace Express { + interface Request { + session?: Session; + user?: User; + } + } +} + +interface ApiResponse { + status: string; + message?: string; + data?: T; +} +const send = (res: Response, code: number, body: ApiResponse) => + res.status(code).json(body); + +// Utility to get month boundaries in local time (00:00:00.000 inclusive to next month start exclusive) +const monthRange = (year: number, month0: number) => { + const start = new Date(year, month0, 1, 0, 0, 0, 0); + const end = new Date(year, month0 + 1, 1, 0, 0, 0, 0); + return { start, end }; +}; + +/** + * Returns an array of booleans (index 0 = day 1) for the requested month indicating + * which days the user submitted a quiz (DailyQuiz.submittedAt not null). + * Query params: year=YYYY, month=1-12 (defaults to current year/month if omitted) + */ +export const getQuizSubmissionCalendar = async ( + req: Request, + res: Response +): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + + // Parse month/year with fallbacks + const now = new Date(); + const year = Number(req.query.year) || now.getFullYear(); + const monthParam = Number(req.query.month); // 1-12 + const month0 = + monthParam && monthParam >= 1 && monthParam <= 12 ? + monthParam - 1 + : now.getMonth(); + + try { + const { start, end } = monthRange(year, month0); + const daysInMonth = new Date(year, month0 + 1, 0).getDate(); + const days: boolean[] = Array(daysInMonth).fill(false); + + const submissions = await prisma.dailyQuiz.findMany({ + where: { + userId, + submittedAt: { not: null, gte: start, lt: end }, + }, + select: { submittedAt: true }, + }); + + for (const s of submissions) { + if (!s.submittedAt) continue; + const day = s.submittedAt.getDate(); // 1-based + if (day >= 1 && day <= daysInMonth) days[day - 1] = true; + } + + send(res, 200, { + status: "success", + data: { year, month: month0 + 1, days }, + }); + } catch (err) { + console.error("[getQuizSubmissionCalendar] error", err); + send(res, 500, { + status: "error", + message: "Failed to fetch calendar", + }); + } +}; diff --git a/apps/api/src/controller/quiz.controller.ts b/apps/api/src/controller/quiz.controller.ts new file mode 100644 index 0000000..df66ce1 --- /dev/null +++ b/apps/api/src/controller/quiz.controller.ts @@ -0,0 +1,130 @@ +import { Request, Response } from "express"; +import type { Session, User } from "better-auth"; +import { fetchAiRecommendation } from "../services/ai.service.js"; +import { buildUserQuizData } from "../services/quiz-data.service.js"; +import { fetchQuestionsByRecommendation } from "../services/question.service.js"; +import { + saveOrUpdateDailyQuiz, + findTodayDailyQuiz, +} from "../services/daily-quiz.service.js"; +import { + gradeAnswers, + SubmittedAnswerInput, +} from "../services/quiz-submission.service.js"; +import { updateDailyQuizScoreByUserToday } from "../services/daily-quiz.service.js"; + +declare global { + namespace Express { + interface Request { + session?: Session; + user?: User; + } + } +} + +// Utility helpers ------------------------------------------------------------- +const startOfDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate()); + +interface ApiResponse { + status: string; + message?: string; + data?: T; +} +const send = (res: Response, code: number, body: ApiResponse) => + res.status(code).json(body); + +export const getQuiz = async (req: Request, res: Response): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + const today = new Date(); + + try { + // Fetch existing quiz (by date range) if any + const existingQuiz = await findTodayDailyQuiz(userId, today); + const totalQuestions = existingQuiz?.totalQuestions || 10; + + // Build data for AI + const quizData = await buildUserQuizData(userId, totalQuestions); + if (!quizData) { + send(res, 404, { + status: "fail", + message: "User hasn't completed any topics yet", + }); + return; + } + + const aiRecommendation = await fetchAiRecommendation(quizData); + const questions = await fetchQuestionsByRecommendation( + aiRecommendation, + totalQuestions + ); + + // Persist / update quiz record (schema only stores counts currently) + const savedQuiz = await saveOrUpdateDailyQuiz( + userId, + startOfDay(today), + totalQuestions + ); + + send(res, 200, { + status: "success", + data: { quiz: savedQuiz, questions, aiRecommendation }, + }); + } catch (err) { + console.error("[getQuiz] error", err); + send(res, 500, { + status: "error", + message: "Failed to generate quiz", + }); + } +}; + +export const submitQuiz = async ( + req: Request, + res: Response +): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + const today = new Date(); + + try { + const { answers } = req.body as { answers: SubmittedAnswerInput[] }; + if (!Array.isArray(answers) || answers.length === 0) { + send(res, 400, { + status: "fail", + message: "answers array required", + }); + return; + } + + const grading = await gradeAnswers(answers); + await updateDailyQuizScoreByUserToday( + userId, + today, + grading.scorePercentage + ); + + send(res, 200, { + status: "success", + data: { + score: grading.scorePercentage, + correctCount: grading.correctCount, + total: grading.total, + answers: grading.graded, + }, + }); + } catch (err) { + console.error("[submitQuiz] error", err); + send(res, 500, { + status: "error", + message: "Failed to submit quiz", + }); + } +}; diff --git a/apps/api/src/routes/quizzes.ts b/apps/api/src/routes/quizzes.ts index 06dbcac..986ae87 100644 --- a/apps/api/src/routes/quizzes.ts +++ b/apps/api/src/routes/quizzes.ts @@ -1,23 +1,29 @@ import { Router } from "express"; import { validate } from "../middlewares/validate.js"; +import { requireAuth } from "../middlewares/auth.js"; import { submitDailyQuizBodySchema } from "../schemas/quizzes.js"; +import { getQuiz, submitQuiz } from "../controller/quiz.controller.js"; +import { getQuizSubmissionCalendar } from "../controller/calendar.controller.js"; const router = Router(); -router.get("/monthly-stats", (req, res) => { - res.send(req.url); -}); +// Calendar of submissions (month view) +router.get( + "/calendar", + requireAuth, + // optional validation for query could be added later + getQuizSubmissionCalendar +); -router.get("/daily", (req, res) => { - res.send(req.url); -}); +// Fetch / (re)generate today's quiz for the user +router.get("/daily", requireAuth, getQuiz); +// Submit answers for today's quiz router.post( "/daily", + requireAuth, validate({ body: submitDailyQuizBodySchema }), - (req, res) => { - res.send(req.url); - }, + submitQuiz ); export { router as quizzesRouter }; diff --git a/apps/api/src/schemas/quizzes.ts b/apps/api/src/schemas/quizzes.ts index 1e6e4e9..226288a 100644 --- a/apps/api/src/schemas/quizzes.ts +++ b/apps/api/src/schemas/quizzes.ts @@ -1,10 +1,12 @@ import z from "zod"; export const submitDailyQuizBodySchema = z.object({ - answers: z.array( - z.object({ - questionId: z.string().uuid(), - answer: z.string().optional(), - }), - ), + answers: z + .array( + z.object({ + questionId: z.string().uuid(), + choiceIndex: z.number().int().min(0), + }) + ) + .min(1), }); diff --git a/apps/api/src/services/ai.service.ts b/apps/api/src/services/ai.service.ts new file mode 100644 index 0000000..f37d205 --- /dev/null +++ b/apps/api/src/services/ai.service.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +export const fetchAiRecommendation = async (quizData: any) => { + try { + const aiApiUrl = process.env.AI_API_URL || "http://localhost:5000/api/data"; + const response = await axios.post( + aiApiUrl, + quizData + ); + return response.data; + } catch (err) { + console.error("AI Recommendation error:", err); + throw new Error("Failed to get AI recommendation"); + } +}; diff --git a/apps/api/src/services/daily-quiz.service.ts b/apps/api/src/services/daily-quiz.service.ts new file mode 100644 index 0000000..c24acf7 --- /dev/null +++ b/apps/api/src/services/daily-quiz.service.ts @@ -0,0 +1,67 @@ +// Local prisma client +import { prisma } from "../lib/prisma.js"; + +const startOfDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate()); +const nextDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1); + +export const findTodayDailyQuiz = async ( + userId: string, + refDate = new Date() +) => { + return prisma.dailyQuiz.findFirst({ + where: { + userId, + createdAt: { gte: startOfDay(refDate), lt: nextDay(refDate) }, + }, + orderBy: { createdAt: "desc" }, + }); +}; + +export const createDailyQuiz = async ( + userId: string, + totalQuestions: number +) => { + return prisma.dailyQuiz.create({ + data: { userId, totalQuestions, score: 0 }, + }); +}; + +export const saveOrUpdateDailyQuiz = async ( + userId: string, + refDate: Date, + totalQuestions: number +) => { + const existing = await findTodayDailyQuiz(userId, refDate); + if (existing) { + if (existing.totalQuestions !== totalQuestions) { + return prisma.dailyQuiz.update({ + where: { id: existing.id }, + data: { totalQuestions }, + }); + } + return existing; + } + return createDailyQuiz(userId, totalQuestions); +}; + +export const submitDailyQuiz = async (quizId: string, score: number) => { + return prisma.dailyQuiz.update({ + where: { id: quizId }, + data: { score, submittedAt: new Date() }, + }); +}; + +export const updateDailyQuizScoreByUserToday = async ( + userId: string, + refDate: Date, + score: number +) => { + const quiz = await findTodayDailyQuiz(userId, refDate); + if (!quiz) return null; + return prisma.dailyQuiz.update({ + where: { id: quiz.id }, + data: { score, submittedAt: new Date() }, + }); +}; diff --git a/apps/api/src/services/question.service.ts b/apps/api/src/services/question.service.ts new file mode 100644 index 0000000..db3f88b --- /dev/null +++ b/apps/api/src/services/question.service.ts @@ -0,0 +1,25 @@ +import { prisma } from "../lib/prisma.js"; + +export const fetchQuestionsByRecommendation = async ( + aiRecommendation: any, + totalQuestions: number +) => { + const levelsToFetch = aiRecommendation.topics.flatMap((topic: any) => + topic.recommendations.map((rec: any) => rec.level) + ); + + return prisma.question.findMany({ + where: { + topics: { + some: { + course: { + level: { + title: { in: levelsToFetch.map((lvl: number) => `Level ${lvl}`) }, + }, + }, + }, + }, + }, + take: totalQuestions, + }); +}; diff --git a/apps/api/src/services/quiz-data.service.ts b/apps/api/src/services/quiz-data.service.ts new file mode 100644 index 0000000..a29e4a1 --- /dev/null +++ b/apps/api/src/services/quiz-data.service.ts @@ -0,0 +1,97 @@ +import { prisma } from "../lib/prisma.js"; + +export const buildUserQuizData = async ( + userId: string, + totalQuestions: number +) => { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + userCompletions: { + include: { + topic: { + include: { + course: { include: { level: true } }, + questions: true, + }, + }, + }, + }, + }, + }); + + const userCompletions = user?.userCompletions; + if (!userCompletions || userCompletions.length === 0) { + return null; + } + + const topicMap = new Map< + string, + { topic: string; available: { level: number; count: number }[] } + >(); + const progressMap = new Map< + string, + { + topic: string; + progressByLevel: { level: number; solved: number; attempted: number }[]; + } + >(); + + for (const completion of userCompletions) { + const topic = completion.topic; + const course = topic.course; + const levelNumber = parseInt(course.level.title.match(/\d+/)?.[0] || "0"); + + // Build userTopics + if (!topicMap.has(topic.title)) { + topicMap.set(topic.title, { + topic: topic.title, + available: [{ level: levelNumber, count: topic.questions.length }], + }); + } else { + const available = topicMap.get(topic.title)!.available; + const levelEntry = available.find((a) => a.level === levelNumber); + if (levelEntry) { + levelEntry.count += topic.questions.length; + } else { + available.push({ level: levelNumber, count: topic.questions.length }); + } + } + + // Build userProgress + if (!progressMap.has(topic.title)) { + progressMap.set(topic.title, { + topic: topic.title, + progressByLevel: [ + { + level: levelNumber, + solved: topic.solved, + attempted: topic.attempted, + }, + ], + }); + } else { + const progressByLevel = progressMap.get(topic.title)!.progressByLevel; + const progressEntry = progressByLevel.find( + (p) => p.level === levelNumber + ); + if (progressEntry) { + progressEntry.solved += topic.solved; + progressEntry.attempted += topic.attempted; + } else { + progressByLevel.push({ + level: levelNumber, + solved: topic.solved, + attempted: topic.attempted, + }); + } + } + } + + return { + userId, + totalQuestions, + userTopics: Array.from(topicMap.values()), + userProgress: Array.from(progressMap.values()), + }; +}; diff --git a/apps/api/src/services/quiz-submission.service.ts b/apps/api/src/services/quiz-submission.service.ts new file mode 100644 index 0000000..f957198 --- /dev/null +++ b/apps/api/src/services/quiz-submission.service.ts @@ -0,0 +1,77 @@ +import { prisma } from "../lib/prisma.js"; + +export interface SubmittedAnswerInput { + questionId: string; + choiceIndex: number; +} + +export interface GradedAnswerResult { + questionId: string; + userChoiceIndex: number | null; + correctOptionIndex: number; + isCorrect: boolean; +} + +export interface GradeQuizResult { + graded: GradedAnswerResult[]; + correctCount: number; + total: number; + scorePercentage: number; // 0-100 +} + +const INVALID_QUESTION_INDEX = -1; + +/** + * Grades a batch of answers. Missing or invalid questions are ignored but still counted toward total if they existed in input. + */ +export const gradeAnswers = async ( + answers: SubmittedAnswerInput[] +): Promise => { + // Dedupe by questionId (keep last answer provided by user) + const map = new Map(); + for (const a of answers) { + if (a && a.questionId) map.set(a.questionId, a); + } + const uniqueAnswers = Array.from(map.values()); + if (uniqueAnswers.length === 0) { + return { graded: [], correctCount: 0, total: 0, scorePercentage: 0 }; + } + + const questionIds = uniqueAnswers.map((a) => a.questionId); + const questions = await prisma.question.findMany({ + where: { id: { in: questionIds } }, + select: { id: true, correctOptionIndex: true }, + }); + const questionMap = new Map< + string, + { id: string; correctOptionIndex: number } + >( + questions.map((q: { id: string; correctOptionIndex: number }) => [q.id, q]) + ); + + const graded: GradedAnswerResult[] = uniqueAnswers.map((a) => { + const q = questionMap.get(a.questionId); + if (!q) { + return { + questionId: a.questionId, + userChoiceIndex: a.choiceIndex ?? null, + correctOptionIndex: INVALID_QUESTION_INDEX, + isCorrect: false, + }; + } + const isCorrect = a.choiceIndex === q.correctOptionIndex; + return { + questionId: q.id, + userChoiceIndex: a.choiceIndex ?? null, + correctOptionIndex: q.correctOptionIndex, + isCorrect, + }; + }); + + const correctCount = graded.filter((g) => g.isCorrect).length; + const total = graded.length; + const scorePercentage = + total === 0 ? 0 : +((correctCount / total) * 100).toFixed(2); + + return { graded, correctCount, total, scorePercentage }; +}; diff --git a/apps/api/src/services/quizzes.ts b/apps/api/src/services/quizzes.ts deleted file mode 100644 index 2288cc0..0000000 --- a/apps/api/src/services/quizzes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const generateNewQuiz = () => { - console.log("Generating a new quiz..."); -}; diff --git a/apps/api/src/services/recommendations.ts b/apps/api/src/services/recommendations.ts deleted file mode 100644 index 63a8bd7..0000000 --- a/apps/api/src/services/recommendations.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getRecommendations = () => { - console.log("Getting recommendations..."); -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d1a243..86d32e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: dependencies: '@adminjs/express': specifier: ^6.1.1 - version: 6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1) + version: 6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1) '@prisma/client': specifier: 6.11.1 version: 6.11.1(prisma@6.12.0(typescript@5.8.2))(typescript@5.8.2) @@ -34,7 +34,10 @@ importers: version: 2.1.13(@tiptap/core@2.1.13(@tiptap/pm@2.1.13))(@tiptap/pm@2.1.13) adminjs: specifier: ^7.8.17 - version: 7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8) + version: 7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8) + axios: + specifier: ^1.11.0 + version: 1.11.0 better-auth: specifier: ^1.2.12 version: 1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -173,7 +176,7 @@ importers: version: 9.32.0 '@tailwindcss/vite': specifier: ^4.1.5 - version: 4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -194,7 +197,7 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -233,16 +236,16 @@ importers: version: 8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.7.3) vite: specifier: ^6.3.1 - version: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + version: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) vitest: specifier: ^3.1.2 - version: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3) + version: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) packages: @@ -5688,9 +5691,9 @@ snapshots: - react-is - supports-color - '@adminjs/express@6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1)': + '@adminjs/express@6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1)': dependencies: - adminjs: 7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8) + adminjs: 7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8) express: 4.21.2 express-formidable: 1.2.0 express-session: 1.18.2 @@ -6963,7 +6966,7 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@hello-pangea/dnd@16.6.0(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@hello-pangea/dnd@16.6.0(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.2 css-box-model: 1.2.1 @@ -6971,7 +6974,7 @@ snapshots: raf-schd: 4.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux: 8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) + react-redux: 8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) redux: 4.2.1 use-memo-one: 1.1.3(react@18.3.1) transitivePeerDependencies: @@ -7736,12 +7739,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) '@tanstack/query-core@5.83.0': {} @@ -8282,7 +8285,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -8290,7 +8293,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - supports-color @@ -8302,13 +8305,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -8349,7 +8352,7 @@ snapshots: acorn@8.15.0: {} - adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8): + adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8): dependencies: '@adminjs/design-system': 4.1.1(@babel/core@7.28.0)(@types/react@19.1.8)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1) '@babel/core': 7.28.0 @@ -8360,7 +8363,7 @@ snapshots: '@babel/preset-react': 7.27.1(@babel/core@7.28.0) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) '@babel/register': 7.27.1(@babel/core@7.28.0) - '@hello-pangea/dnd': 16.6.0(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@hello-pangea/dnd': 16.6.0(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@redux-devtools/extension': 3.3.0(redux@4.2.1) '@rollup/plugin-babel': 6.0.4(@babel/core@7.28.0)(@types/babel__core@7.20.5)(rollup@4.40.2) '@rollup/plugin-commonjs': 25.0.8(rollup@4.40.2) @@ -8383,7 +8386,7 @@ snapshots: react-feather: 2.0.10(react@18.3.1) react-i18next: 12.3.1(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-is: 18.3.1 - react-redux: 8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) + react-redux: 8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) react-router: 6.30.1(react@18.3.1) react-router-dom: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) redux: 4.2.1 @@ -10601,7 +10604,7 @@ snapshots: react-fast-compare: 3.2.2 warning: 4.0.3 - react-redux@8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): + react-redux@8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): dependencies: '@babel/runtime': 7.28.2 '@types/hoist-non-react-statics': 3.3.7(@types/react@19.1.8) @@ -10612,6 +10615,7 @@ snapshots: use-sync-external-store: 1.5.0(react@18.3.1) optionalDependencies: '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) react-dom: 18.3.1(react@18.3.1) redux: 4.2.1 @@ -11424,13 +11428,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3): + vite-node@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@5.5.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - '@types/node' - jiti @@ -11445,29 +11449,29 @@ snapshots: - tsx - yaml - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)): + vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@svgr/core': 8.1.0(typescript@5.7.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3)) - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.7.3) optionalDependencies: - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3): + vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.3) @@ -11481,12 +11485,13 @@ snapshots: jiti: 2.5.1 lightningcss: 1.30.1 tsx: 4.20.3 + yaml: 1.10.2 - vitest@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3): + vitest@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -11504,8 +11509,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) - vite-node: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) + vite-node: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.16.5