Skip to content

Commit 285bec1

Browse files
committed
feat: mvp of multichoice quiz
1 parent e71a3e4 commit 285bec1

File tree

10 files changed

+501
-76
lines changed

10 files changed

+501
-76
lines changed

frontend/app/api/generated.ts

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2420,12 +2420,52 @@ export type StartQuizMutationVariables = Exact<{
24202420

24212421

24222422
export type StartQuizMutation = { __typename?: 'Mutation', startQuiz: { __typename?: 'QuizSubmission', id: string, startedAt: any, expiresAt?: any | null, isExpired: boolean, questionOrder: Array<string>, orderedQuestions: Array<
2423-
| { __typename?: 'FreeTextQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2424-
| { __typename?: 'JsonQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2425-
| { __typename?: 'NumberQuestion', minValue?: number | null, maxValue?: number | null, stepValue?: number | null, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2426-
| { __typename?: 'PredefinedQuestion', allowMultipleSelection: boolean, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null, predefinedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, answerOrder: number, isCorrect?: boolean | null }> }
2423+
| { __typename: 'FreeTextQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2424+
| { __typename: 'JsonQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2425+
| { __typename: 'NumberQuestion', minValue?: number | null, maxValue?: number | null, stepValue?: number | null, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
2426+
| { __typename: 'PredefinedQuestion', allowMultipleSelection: boolean, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null, predefinedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, answerOrder: number, isCorrect?: boolean | null }> }
24272427
>, quiz: { __typename?: 'Quiz', id: string, name: string, timeoutSeconds?: number | null } } };
24282428

2429+
export type SubmitQuizAnswerMutationVariables = Exact<{
2430+
submissionId: Scalars['ID']['input'];
2431+
input: SubmitQuizAnswerInput;
2432+
}>;
2433+
2434+
2435+
export type SubmitQuizAnswerMutation = { __typename?: 'Mutation', submitQuizAnswer:
2436+
| { __typename: 'FreeTextResponse', id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2437+
| { __typename?: 'FreeTextQuestion', id: string }
2438+
| { __typename?: 'JsonQuestion', id: string }
2439+
| { __typename?: 'NumberQuestion', id: string }
2440+
| { __typename?: 'PredefinedQuestion', id: string }
2441+
}
2442+
| { __typename: 'JsonResponse', id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2443+
| { __typename?: 'FreeTextQuestion', id: string }
2444+
| { __typename?: 'JsonQuestion', id: string }
2445+
| { __typename?: 'NumberQuestion', id: string }
2446+
| { __typename?: 'PredefinedQuestion', id: string }
2447+
}
2448+
| { __typename: 'NumberResponse', id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2449+
| { __typename?: 'FreeTextQuestion', id: string }
2450+
| { __typename?: 'JsonQuestion', id: string }
2451+
| { __typename?: 'NumberQuestion', id: string }
2452+
| { __typename?: 'PredefinedQuestion', id: string }
2453+
}
2454+
| { __typename: 'PredefinedResponse', isCorrect?: boolean | null, selectedAnswerIds: Array<string>, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, selectedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, isCorrect?: boolean | null }>, question:
2455+
| { __typename?: 'FreeTextQuestion', id: string }
2456+
| { __typename?: 'JsonQuestion', id: string }
2457+
| { __typename?: 'NumberQuestion', id: string }
2458+
| { __typename?: 'PredefinedQuestion', id: string }
2459+
}
2460+
};
2461+
2462+
export type FinalizeQuizMutationVariables = Exact<{
2463+
submissionId: Scalars['ID']['input'];
2464+
}>;
2465+
2466+
2467+
export type FinalizeQuizMutation = { __typename?: 'Mutation', finalizeQuiz: { __typename?: 'QuizSubmission', id: string, completedAt?: any | null, score?: number | null, maxScore?: number | null, scorePercentage?: number | null, pointsAwarded?: number | null } };
2468+
24292469
export type AssignRoleMutationVariables = Exact<{
24302470
input: AssignRoleInput;
24312471
}>;
@@ -2524,16 +2564,36 @@ export type ChallengePageQueryVariables = Exact<{
25242564

25252565
export type ChallengePageQuery = { __typename?: 'Query', challenge:
25262566
| { __typename: 'ExternalChallenge', url: string, id: string, name: string, description: any, userEnrolledAt?: any | null, userCompletedAt?: any | null }
2527-
| { __typename: 'QuizChallenge', id: string, name: string, description: any, userEnrolledAt?: any | null, userCompletedAt?: any | null, quiz: { __typename?: 'Quiz', id: string, name: string, description: string, timeoutSeconds?: number | null, randomizeQuestions: boolean, revealCorrectAnswers: boolean, allowRetakes: boolean, completionPoints: number, publishedAt?: any | null, endTime?: any | null, userCanStart: boolean, userActiveSubmission?: { __typename?: 'QuizSubmission', id: string } | null, userSubmissions: Array<{ __typename?: 'QuizSubmission', id: string, startedAt: any, completedAt?: any | null, expiresAt?: any | null, isExpired: boolean, score?: number | null, maxScore?: number | null, scorePercentage?: number | null, orderedQuestions: Array<
2567+
| { __typename: 'QuizChallenge', id: string, name: string, description: any, userEnrolledAt?: any | null, userCompletedAt?: any | null, quiz: { __typename?: 'Quiz', id: string, name: string, description: string, timeoutSeconds?: number | null, randomizeQuestions: boolean, revealCorrectAnswers: boolean, allowRetakes: boolean, completionPoints: number, publishedAt?: any | null, endTime?: any | null, userCanStart: boolean, userActiveSubmission?: { __typename?: 'QuizSubmission', id: string } | null, userSubmissions: Array<{ __typename?: 'QuizSubmission', id: string, startedAt: any, completedAt?: any | null, expiresAt?: any | null, isExpired: boolean, score?: number | null, maxScore?: number | null, scorePercentage?: number | null, pointsAwarded?: number | null, orderedQuestions: Array<
25282568
| { __typename: 'FreeTextQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
25292569
| { __typename: 'JsonQuestion', id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
25302570
| { __typename: 'NumberQuestion', minValue?: number | null, maxValue?: number | null, stepValue?: number | null, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null }
25312571
| { __typename: 'PredefinedQuestion', allowMultipleSelection: boolean, id: string, questionText: string, questionOrder: number, timeoutSeconds?: number | null, predefinedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, answerOrder: number, isCorrect?: boolean | null }> }
25322572
>, responses: Array<
2533-
| { __typename: 'FreeTextResponse', textResponse: string, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null }
2534-
| { __typename: 'JsonResponse', jsonResponse: any, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null }
2535-
| { __typename: 'NumberResponse', numberResponse: number, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null }
2536-
| { __typename: 'PredefinedResponse', isCorrect?: boolean | null, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, selectedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, answerOrder: number, isCorrect?: boolean | null }> }
2573+
| { __typename: 'FreeTextResponse', textResponse: string, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2574+
| { __typename?: 'FreeTextQuestion', id: string }
2575+
| { __typename?: 'JsonQuestion', id: string }
2576+
| { __typename?: 'NumberQuestion', id: string }
2577+
| { __typename?: 'PredefinedQuestion', id: string }
2578+
}
2579+
| { __typename: 'JsonResponse', jsonResponse: any, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2580+
| { __typename?: 'FreeTextQuestion', id: string }
2581+
| { __typename?: 'JsonQuestion', id: string }
2582+
| { __typename?: 'NumberQuestion', id: string }
2583+
| { __typename?: 'PredefinedQuestion', id: string }
2584+
}
2585+
| { __typename: 'NumberResponse', numberResponse: number, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, question:
2586+
| { __typename?: 'FreeTextQuestion', id: string }
2587+
| { __typename?: 'JsonQuestion', id: string }
2588+
| { __typename?: 'NumberQuestion', id: string }
2589+
| { __typename?: 'PredefinedQuestion', id: string }
2590+
}
2591+
| { __typename: 'PredefinedResponse', isCorrect?: boolean | null, id: string, answeredAt?: any | null, timeSpentSeconds?: number | null, selectedAnswers: Array<{ __typename?: 'QuizPredefinedAnswer', id: string, answerText: string, answerOrder: number, isCorrect?: boolean | null }>, question:
2592+
| { __typename?: 'FreeTextQuestion', id: string }
2593+
| { __typename?: 'JsonQuestion', id: string }
2594+
| { __typename?: 'NumberQuestion', id: string }
2595+
| { __typename?: 'PredefinedQuestion', id: string }
2596+
}
25372597
> }> } }
25382598
| { __typename: 'SimpleChallenge', allowSelfCompletion: boolean, id: string, name: string, description: any, userEnrolledAt?: any | null, userCompletedAt?: any | null }
25392599
};
@@ -3015,6 +3075,7 @@ export const StartQuizDocument = gql`
30153075
isExpired
30163076
questionOrder
30173077
orderedQuestions {
3078+
__typename
30183079
id
30193080
questionText
30203081
questionOrder
@@ -3046,6 +3107,48 @@ export const StartQuizDocument = gql`
30463107
export function useStartQuizMutation() {
30473108
return Urql.useMutation<StartQuizMutation, StartQuizMutationVariables>(StartQuizDocument);
30483109
};
3110+
export const SubmitQuizAnswerDocument = gql`
3111+
mutation SubmitQuizAnswer($submissionId: ID!, $input: SubmitQuizAnswerInput!) {
3112+
submitQuizAnswer(submissionId: $submissionId, input: $input) {
3113+
__typename
3114+
id
3115+
answeredAt
3116+
timeSpentSeconds
3117+
question {
3118+
id
3119+
}
3120+
... on PredefinedResponse {
3121+
isCorrect
3122+
selectedAnswerIds
3123+
selectedAnswers {
3124+
id
3125+
answerText
3126+
isCorrect
3127+
}
3128+
}
3129+
}
3130+
}
3131+
`;
3132+
3133+
export function useSubmitQuizAnswerMutation() {
3134+
return Urql.useMutation<SubmitQuizAnswerMutation, SubmitQuizAnswerMutationVariables>(SubmitQuizAnswerDocument);
3135+
};
3136+
export const FinalizeQuizDocument = gql`
3137+
mutation FinalizeQuiz($submissionId: ID!) {
3138+
finalizeQuiz(submissionId: $submissionId) {
3139+
id
3140+
completedAt
3141+
score
3142+
maxScore
3143+
scorePercentage
3144+
pointsAwarded
3145+
}
3146+
}
3147+
`;
3148+
3149+
export function useFinalizeQuizMutation() {
3150+
return Urql.useMutation<FinalizeQuizMutation, FinalizeQuizMutationVariables>(FinalizeQuizDocument);
3151+
};
30493152
export const AssignRoleDocument = gql`
30503153
mutation AssignRole($input: AssignRoleInput!) {
30513154
assignRole(input: $input) {
@@ -3218,7 +3321,7 @@ export const ChallengePageDocument = gql`
32183321
score
32193322
maxScore
32203323
scorePercentage
3221-
scorePercentage
3324+
pointsAwarded
32223325
orderedQuestions {
32233326
__typename
32243327
id
@@ -3245,6 +3348,9 @@ export const ChallengePageDocument = gql`
32453348
id
32463349
answeredAt
32473350
timeSpentSeconds
3351+
question {
3352+
id
3353+
}
32483354
... on FreeTextResponse {
32493355
textResponse
32503356
}
Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,105 @@
11
<script setup lang="ts">
2-
import type { QuizChallengeData } from './quiz/types'
2+
import type { QuizChallengeData, QuestionResult } from './quiz/types'
3+
import type { FinalizeQuizMutation, StartQuizMutation } from '~/api/generated'
34
45
const props = defineProps<{
56
challenge: QuizChallengeData
67
}>()
78
89
const emit = defineEmits<{
910
start: []
11+
complete: []
1012
}>()
1113
1214
const { executeMutation: startQuiz } = useStartQuizMutation()
15+
const { executeMutation: finalizeQuiz } = useFinalizeQuizMutation()
1316
14-
onMounted(() => {
15-
if (!props.challenge.quiz.userActiveSubmission?.id) {
16-
startQuiz({
17+
const currentQuestionIndex = ref(0)
18+
const questionResults = ref<QuestionResult[]>([])
19+
const quizCompleted = ref(false)
20+
const finalResult = ref<FinalizeQuizMutation['finalizeQuiz'] | null>(null)
21+
const startedSubmission = ref<StartQuizMutation['startQuiz'] | null>(null)
22+
const isLoading = ref(false)
23+
24+
// Check if user has a completed submission (quiz already taken)
25+
const completedSubmission = computed(() => {
26+
return props.challenge.quiz.userSubmissions.find((s) => s.completedAt)
27+
})
28+
29+
// Build results from a completed submission's responses
30+
const completedSubmissionResults = computed<QuestionResult[]>(() => {
31+
if (!completedSubmission.value) return []
32+
33+
return completedSubmission.value.responses.map((response) => ({
34+
questionId: response.question.id,
35+
isCorrect:
36+
response.__typename === 'PredefinedResponse'
37+
? (response.isCorrect ?? null)
38+
: null,
39+
}))
40+
})
41+
42+
// Check if we can start a new quiz
43+
const canStartQuiz = computed(() => {
44+
return props.challenge.quiz.userCanStart
45+
})
46+
47+
onMounted(async () => {
48+
// If there's no active submission and we can start, start the quiz
49+
if (!props.challenge.quiz.userActiveSubmission?.id && canStartQuiz.value) {
50+
isLoading.value = true
51+
const result = await startQuiz({
1752
quizId: props.challenge.quiz.id,
18-
}).then(() => {
19-
emit('start')
2053
})
54+
if (result.data?.startQuiz) {
55+
startedSubmission.value = result.data.startQuiz
56+
}
57+
isLoading.value = false
58+
emit('start')
2159
}
2260
})
2361
2462
const activeSubmission = computed(() => {
63+
// Use the started submission if we just started, otherwise find from existing submissions
64+
if (startedSubmission.value) {
65+
return startedSubmission.value
66+
}
2567
return props.challenge.quiz.userSubmissions.find(
2668
(submission) =>
2769
submission.id === props.challenge.quiz.userActiveSubmission?.id,
2870
)
2971
})
3072
3173
const questions = computed(() => {
32-
if (props.challenge.quiz.randomizeQuestions) {
33-
return activeSubmission.value?.orderedQuestions.toSorted(
34-
() => Math.random() - 0.5,
35-
)
36-
}
37-
return activeSubmission.value?.orderedQuestions
74+
return activeSubmission.value?.orderedQuestions ?? []
75+
})
76+
77+
const currentQuestion = computed(() => {
78+
return questions.value[currentQuestionIndex.value]
3879
})
80+
81+
const isLastQuestion = computed(() => {
82+
return currentQuestionIndex.value === questions.value.length - 1
83+
})
84+
85+
async function handleAnswerSubmitted(result: QuestionResult) {
86+
questionResults.value.push(result)
87+
88+
if (isLastQuestion.value) {
89+
if (activeSubmission.value) {
90+
const response = await finalizeQuiz({
91+
submissionId: activeSubmission.value.id,
92+
})
93+
if (response.data?.finalizeQuiz) {
94+
finalResult.value = response.data.finalizeQuiz
95+
quizCompleted.value = true
96+
emit('complete')
97+
}
98+
}
99+
} else {
100+
currentQuestionIndex.value++
101+
}
102+
}
39103
</script>
40104

41105
<template>
@@ -46,29 +110,60 @@ const questions = computed(() => {
46110
</NuxtLink>
47111
</template>
48112
<template #title>
49-
<QuizProgress v-if="activeSubmission" :submission="activeSubmission" />
113+
<QuizProgress
114+
v-if="activeSubmission && !quizCompleted"
115+
:current-index="currentQuestionIndex"
116+
:total-questions="questions.length"
117+
:results="questionResults"
118+
/>
119+
</template>
120+
121+
<template v-if="isLoading">
122+
<div class="flex items-center justify-center grow">
123+
<LoadingState />
124+
</div>
125+
</template>
126+
127+
<template v-else-if="quizCompleted && finalResult">
128+
<QuizResult
129+
:score="finalResult.score ?? 0"
130+
:max-score="finalResult.maxScore ?? 0"
131+
:points-awarded="finalResult.pointsAwarded ?? 0"
132+
:results="questionResults"
133+
/>
134+
</template>
135+
136+
<template v-else-if="completedSubmission && !canStartQuiz">
137+
<QuizResult
138+
:score="completedSubmission.score ?? 0"
139+
:max-score="completedSubmission.maxScore ?? 0"
140+
:points-awarded="completedSubmission.pointsAwarded ?? 0"
141+
:results="completedSubmissionResults"
142+
/>
50143
</template>
51144

52-
<template v-if="questions?.length">
53-
<template v-for="question in questions" :key="question.id">
54-
<QuizPredefinedQuestion
55-
v-if="question.__typename === 'PredefinedQuestion'"
56-
:question="question"
57-
:total-questions="questions.length"
58-
/>
59-
<QuizNumberQuestion
60-
v-else-if="question.__typename === 'NumberQuestion'"
61-
:question="question"
62-
/>
63-
<QuizJsonQuestion
64-
v-else-if="question.__typename === 'JsonQuestion'"
65-
:question="question"
66-
/>
67-
<QuizFreeTextQuestion
68-
v-else-if="question.__typename === 'FreeTextQuestion'"
69-
:question="question"
70-
/>
71-
</template>
145+
<template v-else-if="currentQuestion">
146+
<QuizPredefinedQuestion
147+
v-if="currentQuestion.__typename === 'PredefinedQuestion'"
148+
:key="currentQuestion.id"
149+
:question="currentQuestion"
150+
:total-questions="questions.length"
151+
:current-index="currentQuestionIndex"
152+
:submission-id="activeSubmission?.id ?? ''"
153+
@answer-submitted="handleAnswerSubmitted"
154+
/>
155+
<QuizNumberQuestion
156+
v-else-if="currentQuestion.__typename === 'NumberQuestion'"
157+
:question="currentQuestion"
158+
/>
159+
<QuizJsonQuestion
160+
v-else-if="currentQuestion.__typename === 'JsonQuestion'"
161+
:question="currentQuestion"
162+
/>
163+
<QuizFreeTextQuestion
164+
v-else-if="currentQuestion.__typename === 'FreeTextQuestion'"
165+
:question="currentQuestion"
166+
/>
72167
</template>
73168
</PageLayout>
74169
</template>

0 commit comments

Comments
 (0)