Files

237 lines
6.5 KiB
TypeScript

import { getAuthUserId } from '@convex-dev/auth/server';
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
export const startQuizSession = mutation({
args: {
questionIds: v.optional(v.array(v.id('questions'))),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
// End any active sessions
const activeSessions = await ctx.db
.query('quizSessions')
.withIndex('by_user_and_active', (q) =>
q.eq('userId', userId).eq('isActive', true),
)
.collect();
for (const session of activeSessions) {
await ctx.db.patch(session._id, { isActive: false });
}
// Get questions
let questionIds = args.questionIds;
if (!questionIds || questionIds.length === 0) {
const allQuestions = await ctx.db.query('questions').collect();
questionIds = allQuestions.map((q) => q._id);
}
// Shuffle questions
const shuffled = [...questionIds].sort(() => Math.random() - 0.5);
return await ctx.db.insert('quizSessions', {
userId,
activeQuestions: shuffled,
missedQuestions: [],
completedQuestions: [],
currentQuestionIndex: 0,
isActive: true,
});
},
});
export const getActiveSession = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const session = await ctx.db
.query('quizSessions')
.withIndex('by_user_and_active', (q) =>
q.eq('userId', userId).eq('isActive', true),
)
.first();
return session;
},
});
export const getCurrentQuestion = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const session = await ctx.db
.query('quizSessions')
.withIndex('by_user_and_active', (q) =>
q.eq('userId', userId).eq('isActive', true),
)
.first();
if (!session) return null;
const allQuestions = [
...session.activeQuestions,
...session.missedQuestions,
];
if (allQuestions.length === 0) return null;
const currentQuestionId = allQuestions[session.currentQuestionIndex];
const question = await ctx.db.get(currentQuestionId);
return {
question,
sessionId: session._id,
progress: {
current: session.currentQuestionIndex + 1,
total: allQuestions.length,
completed: session.completedQuestions.length,
},
};
},
});
export const answerQuestion = mutation({
args: {
sessionId: v.id('quizSessions'),
questionId: v.id('questions'),
selectedAnswer: v.number(),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const session = await ctx.db.get(args.sessionId);
if (!session || session.userId !== userId) {
throw new Error('Invalid session');
}
const question = await ctx.db.get(args.questionId);
if (!question) throw new Error('Question not found');
const isCorrect = args.selectedAnswer === question.correctAnswer;
// Update user progress
const existingProgress = await ctx.db
.query('userProgress')
.withIndex('by_user_and_question', (q) =>
q.eq('userId', userId).eq('questionId', args.questionId),
)
.first();
if (existingProgress) {
await ctx.db.patch(existingProgress._id, {
correctCount: existingProgress.correctCount + (isCorrect ? 1 : 0),
incorrectCount: existingProgress.incorrectCount + (isCorrect ? 0 : 1),
lastAttempted: Date.now(),
});
} else {
await ctx.db.insert('userProgress', {
userId,
questionId: args.questionId,
correctCount: isCorrect ? 1 : 0,
incorrectCount: isCorrect ? 0 : 1,
lastAttempted: Date.now(),
});
}
// Update session
const allQuestions = [
...session.activeQuestions,
...session.missedQuestions,
];
let newActiveQuestions = [...session.activeQuestions];
let newMissedQuestions = [...session.missedQuestions];
let newCompletedQuestions = [...session.completedQuestions];
// Remove current question from active list
const currentIndex = newActiveQuestions.indexOf(args.questionId);
if (currentIndex !== -1) {
newActiveQuestions.splice(currentIndex, 1);
} else {
const missedIndex = newMissedQuestions.indexOf(args.questionId);
if (missedIndex !== -1) {
newMissedQuestions.splice(missedIndex, 1);
}
}
if (isCorrect) {
newCompletedQuestions.push(args.questionId);
} else {
newMissedQuestions.push(args.questionId);
}
const newAllQuestions = [...newActiveQuestions, ...newMissedQuestions];
const newIndex =
newAllQuestions.length > 0 ? 0 : session.currentQuestionIndex;
await ctx.db.patch(args.sessionId, {
activeQuestions: newActiveQuestions,
missedQuestions: newMissedQuestions,
completedQuestions: newCompletedQuestions,
currentQuestionIndex: newIndex,
});
return {
isCorrect,
correctAnswer: question.correctAnswer,
explanation: question.explanation,
};
},
});
export const endQuizSession = mutation({
args: {
sessionId: v.id('quizSessions'),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const session = await ctx.db.get(args.sessionId);
if (!session || session.userId !== userId) {
throw new Error('Invalid session');
}
await ctx.db.patch(args.sessionId, { isActive: false });
return null;
},
});
export const getUserStats = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('Not authenticated');
const progress = await ctx.db
.query('userProgress')
.withIndex('by_user', (q) => q.eq('userId', userId))
.collect();
const totalQuestions = progress.length;
const totalCorrect = progress.reduce((sum, p) => sum + p.correctCount, 0);
const totalIncorrect = progress.reduce(
(sum, p) => sum + p.incorrectCount,
0,
);
const totalAttempts = totalCorrect + totalIncorrect;
return {
totalQuestions,
totalAttempts,
totalCorrect,
totalIncorrect,
accuracy: totalAttempts > 0 ? (totalCorrect / totalAttempts) * 100 : 0,
};
},
});