Skip to content

Commit adb2e7a

Browse files
committed
quiz: different UIs for single and multi select; redux removed
1 parent ad296cb commit adb2e7a

10 files changed

Lines changed: 63 additions & 681 deletions

File tree

apps/web/components/public/lesson-viewer/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export const LessonViewer = ({
310310
<QuizViewer
311311
lessonId={lesson.lessonId}
312312
content={lesson.content as Quiz}
313+
address={address}
313314
/>
314315
)}
315316
{String.prototype.toUpperCase.call(LESSON_TYPE_FILE) ===
Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,31 @@
11
import {
22
Address,
33
Question,
4-
Quiz as QuizViewer,
4+
Quiz as QuizContent,
55
} from "@courselit/common-models";
6-
import {
7-
actionCreators,
8-
AppDispatch,
9-
AppState,
10-
} from "@courselit/state-management";
116
import { FetchBuilder } from "@courselit/utils";
127
import { ChangeEvent, useState } from "react";
13-
import { connect } from "react-redux";
148
import {
159
TOAST_TITLE_ERROR,
16-
QUIZ_FAIL_MESSAGE,
17-
QUIZ_PASS_MESSAGE,
1810
QUIZ_VIEWER_EVALUATE_BTN,
1911
QUIZ_VIEWER_EVALUATE_BTN_LOADING,
20-
TOAST_TITLE_SUCCESS,
21-
} from "../../../ui-config/strings";
12+
TOAST_QUIZ_FAIL_MESSAGE,
13+
TOAST_QUIZ_PASS_MESSAGE,
14+
QUIZ_SCORE_PREFIX_MESSAGE,
15+
} from "@/ui-config/strings";
2216
import { Form, FormSubmit, useToast } from "@courselit/components-library";
2317

24-
const { networkAction } = actionCreators;
25-
2618
interface QuizViewerProps {
2719
lessonId: string;
28-
content: QuizViewer;
29-
dispatch: AppDispatch;
20+
content: QuizContent;
3021
address: Address;
3122
}
3223

33-
function QuizViewer({ content, lessonId, dispatch, address }: QuizViewerProps) {
24+
export default function QuizViewer({
25+
content,
26+
lessonId,
27+
address,
28+
}: QuizViewerProps) {
3429
const { questions } = content;
3530
const [answers, setAnswers] = useState<number[][]>([
3631
...content.questions.map((item) => []),
@@ -43,28 +38,27 @@ function QuizViewer({ content, lessonId, dispatch, address }: QuizViewerProps) {
4338
questionIndex: number,
4439
optionIndex: number,
4540
) => {
46-
const addOptionToQuestion = (
47-
questionIndex: number,
48-
optionIndex: number,
49-
) => {
50-
answers[questionIndex].push(optionIndex);
51-
setAnswers([...answers]);
52-
};
53-
54-
const removeOptionFromQuestion = (
55-
questionIndex: number,
56-
optionIndex: number,
57-
) => {
58-
const index = answers[questionIndex].indexOf(optionIndex);
59-
answers[questionIndex].splice(index, 1);
60-
setAnswers([...answers]);
61-
};
41+
const question = questions[questionIndex];
42+
const newAnswers = [...answers];
6243

63-
if (checked) {
64-
addOptionToQuestion(questionIndex, optionIndex);
44+
if (question.type === "single") {
45+
// For single choice, replace the entire array with the selected option
46+
newAnswers[questionIndex] = checked ? [optionIndex] : [];
6547
} else {
66-
removeOptionFromQuestion(questionIndex, optionIndex);
48+
// For multiple choice, add/remove from the array
49+
if (checked) {
50+
if (!newAnswers[questionIndex].includes(optionIndex)) {
51+
newAnswers[questionIndex].push(optionIndex);
52+
}
53+
} else {
54+
const index = newAnswers[questionIndex].indexOf(optionIndex);
55+
if (index > -1) {
56+
newAnswers[questionIndex].splice(index, 1);
57+
}
58+
}
6759
}
60+
61+
setAnswers(newAnswers);
6862
};
6963

7064
const evaluate = async (e: any) => {
@@ -92,21 +86,20 @@ function QuizViewer({ content, lessonId, dispatch, address }: QuizViewerProps) {
9286
.build();
9387

9488
try {
95-
dispatch(networkAction(true));
9689
setLoading(true);
9790
const response = await fetch.exec();
9891

9992
if (response.result) {
10093
const { pass, score, passingGrade } = response.result;
10194
if (pass) {
10295
toast({
103-
title: TOAST_TITLE_SUCCESS,
104-
description: `${QUIZ_PASS_MESSAGE} ${score} points.`,
96+
title: TOAST_QUIZ_PASS_MESSAGE,
97+
description: `${QUIZ_SCORE_PREFIX_MESSAGE} ${score.toFixed(2)} points.`,
10598
});
10699
} else {
107100
toast({
108-
title: TOAST_TITLE_ERROR,
109-
description: `${QUIZ_FAIL_MESSAGE} ${score} points. Requires ${passingGrade} points.`,
101+
title: TOAST_QUIZ_FAIL_MESSAGE,
102+
description: `${QUIZ_SCORE_PREFIX_MESSAGE} ${score.toFixed(2)} points. Requires ${passingGrade} points.`,
110103
variant: "destructive",
111104
});
112105
}
@@ -118,27 +111,32 @@ function QuizViewer({ content, lessonId, dispatch, address }: QuizViewerProps) {
118111
variant: "destructive",
119112
});
120113
} finally {
121-
dispatch(networkAction(false));
122114
setLoading(false);
123115
}
124116
};
125117

126118
return (
127119
<Form onSubmit={evaluate}>
128120
{questions.map((question: Question, questionIndex: number) => (
129-
<fieldset
130-
className="flex flex-col py-2 px-4 mb-4 border rounded border-slate-200"
131-
key={questionIndex}
132-
>
133-
<h2 className="font-medium text-xl mb-2">
134-
{question.text}
121+
<fieldset className="flex flex-col mb-8" key={questionIndex}>
122+
<h2 className="font-medium mb-2">
123+
{questionIndex + 1}. {question.text}
135124
</h2>
136125
{question.options.map((option, index: number) => (
137126
<div className="flex items-center mb-2" key={index}>
138127
<input
139-
type="checkbox"
128+
type={
129+
question.type === "single"
130+
? "radio"
131+
: "checkbox"
132+
}
140133
className="mr-2"
141-
checked={option.correctAnswer}
134+
name={
135+
question.type === "single"
136+
? `question-${questionIndex}`
137+
: undefined
138+
}
139+
checked={answers[questionIndex].includes(index)}
142140
onChange={(e: ChangeEvent<HTMLInputElement>) =>
143141
setAnswerForQuestion(
144142
e.target.checked,
@@ -165,13 +163,3 @@ function QuizViewer({ content, lessonId, dispatch, address }: QuizViewerProps) {
165163
</Form>
166164
);
167165
}
168-
169-
const mapStateToProps = (state: AppState) => ({
170-
address: state.address,
171-
});
172-
173-
const mapDispatchToProps = (dispatch: AppDispatch) => ({
174-
dispatch: dispatch,
175-
});
176-
177-
export default connect(mapStateToProps, mapDispatchToProps)(QuizViewer);

apps/web/graphql/lessons/helpers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,13 @@ export async function isPartOfDripGroup(
201201
}
202202

203203
export function removeCorrectAnswersProp(lesson: Lesson) {
204-
if (lesson.content && lesson.content.questions) {
205-
for (let question of lesson.content.questions as any[]) {
204+
if (lesson.content && (lesson.content as Quiz).questions) {
205+
for (let question of (lesson.content as Quiz).questions as any[]) {
206+
question.type =
207+
question.options.filter((option: any) => option.correctAnswer)
208+
.length > 1
209+
? "multiple"
210+
: "single";
206211
question.options = question.options.map((option: any) => ({
207212
text: option.text,
208213
}));

apps/web/graphql/lessons/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GraphQLNonNull,
88
GraphQLBoolean,
99
GraphQLInt,
10+
GraphQLFloat,
1011
} from "graphql";
1112
import constants from "../../config/constants";
1213
import mediaTypes from "../media/types";
@@ -121,7 +122,7 @@ const evaluationResult = new GraphQLObjectType({
121122
name: "EvaluationResult",
122123
fields: {
123124
pass: { type: new GraphQLNonNull(GraphQLBoolean) },
124-
score: { type: GraphQLInt },
125+
score: { type: GraphQLFloat },
125126
requiresPassingGrade: { type: new GraphQLNonNull(GraphQLBoolean) },
126127
passingGrade: { type: GraphQLInt },
127128
},

apps/web/pages/404.tsx.notused

Lines changed: 0 additions & 39 deletions
This file was deleted.

apps/web/pages/course.notused/[slug]/[id]/[lesson].tsx

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)