last commit before trying out opencode on repo
This commit is contained in:
236
packages/backend/convex/quiz.ts
Normal file
236
packages/backend/convex/quiz.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user