237 lines
6.5 KiB
TypeScript
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,
|
|
};
|
|
},
|
|
});
|