Skip to content

Commit 691aabc

Browse files
committed
add multi-question flow
1 parent 62613d9 commit 691aabc

File tree

7 files changed

+240
-116
lines changed

7 files changed

+240
-116
lines changed
Lines changed: 106 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,142 @@
11
.quiz {
22

3+
display: flex;
4+
flex-direction: column;
35

4-
ol {
5-
padding: 0;
6+
background-color: var(--site-raised-bgColor-translucent);
7+
border-radius: var(--site-radius);
8+
padding: 1rem;
9+
10+
.quiz-title {
611
margin: 0;
7-
margin-top: 1rem;
8-
list-style: upper-alpha;
9-
list-style-position: inside;
12+
font-size: 1.5rem;
13+
font-weight: 500;
14+
color: var(--site-base-fgColor-lighter);
15+
}
1016

17+
.quiz-progress {
18+
font-size: 0.9rem;
19+
color: var(--site-primary-color);
20+
font-weight: 500;
21+
}
1122

12-
li {
13-
padding: 1rem;
14-
background-color: var(--site-raised-bgColor);
15-
border-radius: var(--site-radius);
16-
margin-bottom: 0.2rem;
17-
transition: background-color 500ms;
23+
.quiz-question {
24+
margin-top: 1.5rem;
25+
display: none;
1826

19-
&:not(:where(.selected, .disabled)):hover {
20-
background-color: var(--site-inset-bgColor);
21-
cursor: pointer;
22-
}
27+
&.active {
28+
display: block;
29+
}
2330

24-
&.selected:has(.correct) {
25-
background-color: oklch(from var(--site-alert-tip-color) l c h / 0.2);
26-
}
31+
ol {
32+
padding: 0;
33+
margin: 0;
34+
margin-top: 1rem;
35+
list-style: upper-alpha;
36+
list-style-position: inside;
2737

28-
&.selected:has(.incorrect) {
29-
background-color: oklch(from var(--site-alert-error-color) l c h / 0.2);
30-
}
3138

32-
&.disabled {
33-
opacity: 0.6;
34-
}
39+
li {
40+
padding: 1rem;
41+
background-color: var(--site-raised-bgColor);
42+
border-radius: var(--site-radius);
43+
margin-bottom: 0.2rem;
44+
transition: background-color 500ms;
3545

36-
p {
37-
margin-bottom: 0;
38-
}
46+
&:not(:where(.selected, .disabled)):hover {
47+
background-color: var(--site-inset-bgColor);
48+
cursor: pointer;
49+
}
3950

40-
.question-wrapper {
41-
display: grid;
42-
grid-template-rows: min-content 0fr;
43-
transition: grid-template-rows 500ms;
44-
}
51+
&.selected:has(.correct) {
52+
background-color: oklch(from var(--site-alert-tip-color) l c h / 0.2);
53+
}
4554

46-
&.selected .question-wrapper {
47-
grid-template-rows: min-content 1fr;
48-
}
55+
&.selected:has(.incorrect) {
56+
background-color: oklch(from var(--site-alert-error-color) l c h / 0.2);
57+
}
4958

50-
.question {
51-
margin-top: -1lh;
52-
margin-left: 1.4rem;
53-
}
59+
&.disabled {
60+
opacity: 0.6;
61+
}
5462

55-
.solution {
56-
position: relative;
57-
padding-left: 1.4rem;
58-
font-size: 0.9rem;
59-
overflow: hidden;
60-
61-
p.correct,
62-
p.incorrect {
63-
padding-top: 0.5rem;
64-
font-weight: 600;
65-
margin-bottom: 0.5rem;
66-
67-
&::before {
68-
position: absolute;
69-
left: 0;
70-
}
63+
p {
64+
margin-bottom: 0;
7165
}
7266

73-
p.correct {
74-
color: green;
67+
.question-wrapper {
68+
display: grid;
69+
grid-template-rows: min-content 0fr;
70+
transition: grid-template-rows 500ms;
71+
}
7572

76-
&::before {
77-
content: "";
78-
}
73+
&.selected .question-wrapper {
74+
grid-template-rows: min-content 1fr;
7975
}
8076

81-
p.incorrect {
82-
color: red;
77+
.question {
78+
margin-top: -1lh;
79+
margin-left: 1.4rem;
80+
}
8381

84-
&::before {
85-
content: "";
82+
.solution {
83+
position: relative;
84+
padding-left: 1.4rem;
85+
font-size: 0.9rem;
86+
overflow: hidden;
87+
88+
p.correct,
89+
p.incorrect {
90+
padding-top: 0.5rem;
91+
font-weight: 600;
92+
margin-bottom: 0.5rem;
93+
94+
&::before {
95+
position: absolute;
96+
left: 0;
97+
}
98+
}
99+
100+
p.correct {
101+
color: green;
102+
103+
&::before {
104+
content: "";
105+
}
106+
}
107+
108+
p.incorrect {
109+
color: red;
110+
111+
&::before {
112+
content: "";
113+
}
86114
}
87115
}
88116
}
89117
}
118+
119+
}
120+
121+
.quiz-complete {
122+
min-height: 15rem;
123+
display: flex;
124+
flex-direction: column;
125+
justify-content: center;
126+
align-items: center;
127+
128+
strong {
129+
font-size: 2rem;
130+
}
90131
}
91132

92133
.quiz-button {
134+
align-self: flex-end;
93135
margin-top: 1rem;
94136

95137
&[disabled] {
96138
opacity: 0.4;
97139
pointer-events: none;
98140
}
99141
}
100-
101-
102142
}

site/lib/jaspr_options.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ Map<String, dynamic> _prefix7DartPadInjector(prefix7.DartPadInjector c) => {
152152
'runAutomatically': c.runAutomatically,
153153
};
154154
Map<String, dynamic> _prefix8InteractiveQuiz(prefix8.InteractiveQuiz c) => {
155-
'question': c.question.toJson(),
155+
'title': c.title,
156+
'questions': c.questions.map((i) => i.toJson()).toList(),
156157
};
157158
Map<String, dynamic> _prefix12ArchiveTable(prefix12.ArchiveTable c) => {
158159
'os': c.os,

site/lib/src/components/fwe/client/quiz.dart

Lines changed: 112 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,137 @@ import '../../common/button.dart';
99

1010
@client
1111
class InteractiveQuiz extends StatefulComponent {
12-
const InteractiveQuiz({required this.question, super.key});
12+
const InteractiveQuiz({
13+
required this.title,
14+
required this.questions,
15+
super.key,
16+
});
1317

14-
final Question question;
18+
final String? title;
19+
final List<Question> questions;
1520

1621
@override
1722
State<InteractiveQuiz> createState() => _InteractiveQuizState();
1823
}
1924

2025
class _InteractiveQuizState extends State<InteractiveQuiz> {
21-
int? selectedOption;
26+
int currentQuestionIndex = 0;
27+
int? selectedOptionIndex;
28+
29+
Question? get currentQuestion {
30+
if (currentQuestionIndex >= component.questions.length) {
31+
return null;
32+
}
33+
return component.questions[currentQuestionIndex];
34+
}
35+
36+
AnswerOption? get selectedOption {
37+
final question = currentQuestion;
38+
if (question == null || selectedOptionIndex == null) {
39+
return null;
40+
}
41+
return question.options[selectedOptionIndex!];
42+
}
2243

2344
@override
2445
Component build(BuildContext context) {
25-
return div([
26-
strong([text(component.question.question)]),
27-
ol([
28-
for (final (index, option) in component.question.options.indexed)
29-
li(
30-
classes: [
31-
if (selectedOption != null)
32-
if (selectedOption == index) 'selected' else 'disabled',
33-
].toClasses,
34-
events: {
35-
'click': (_) {
36-
if (selectedOption != null) {
37-
return;
38-
}
39-
setState(() {
40-
selectedOption = index;
41-
});
42-
},
43-
},
44-
[
45-
div(classes: 'question-wrapper', [
46-
div(classes: 'question', [
47-
p([text(option.text)]),
48-
]),
49-
div(classes: 'solution', [
50-
if (option.correct)
51-
p(classes: 'correct', [text('That\'s right!')])
52-
else
53-
p(classes: 'incorrect', [text('Not quite')]),
54-
p([text(option.explanation)]),
55-
]),
56-
]),
57-
],
58-
),
46+
return div(classes: 'quiz not-content', [
47+
if (component.title case final title?)
48+
h3(classes: 'quiz-title', [
49+
text(title),
50+
]),
51+
span(classes: 'quiz-progress', [
52+
text(
53+
currentQuestion != null
54+
? '${currentQuestionIndex + 1} / ${component.questions.length}'
55+
: 'Complete',
56+
),
5957
]),
58+
for (final question in component.questions)
59+
div(
60+
classes: [
61+
'quiz-question',
62+
if (question == currentQuestion) 'active',
63+
].toClasses,
64+
[
65+
strong([text(question.question)]),
66+
ol([
67+
for (final (index, option) in question.options.indexed)
68+
li(
69+
classes: [
70+
if (option == selectedOption)
71+
'selected'
72+
else if (selectedOption != null)
73+
'disabled',
74+
].toClasses,
75+
events: {
76+
'click': (_) {
77+
if (selectedOption != null) {
78+
return;
79+
}
80+
setState(() {
81+
selectedOptionIndex = index;
82+
});
83+
},
84+
},
85+
[
86+
div(classes: 'question-wrapper', [
87+
div(classes: 'question', [
88+
p([text(option.text)]),
89+
]),
90+
div(classes: 'solution', [
91+
if (option.correct)
92+
p(classes: 'correct', [text('That\'s right!')])
93+
else
94+
p(classes: 'incorrect', [text('Not quite')]),
95+
p([text(option.explanation)]),
96+
]),
97+
]),
98+
],
99+
),
100+
]),
101+
],
102+
),
103+
104+
if (currentQuestion == null)
105+
div(classes: 'quiz-complete', [
106+
strong([text('Great job!')]),
107+
p([text('You completed the quiz.')]),
108+
]),
60109

61110
Button(
62111
classes: ['quiz-button'],
63112
style: ButtonStyle.filled,
64-
disabled: selectedOption == null,
113+
disabled: currentQuestion != null && selectedOption == null,
65114
onClick: () {
115+
if (currentQuestion == null) {
116+
// Restart the quiz.
117+
setState(() {
118+
currentQuestionIndex = 0;
119+
selectedOptionIndex = null;
120+
});
121+
return;
122+
}
123+
if (selectedOption == null) return;
124+
final correct = selectedOption!.correct;
66125
setState(() {
67-
selectedOption = null;
126+
selectedOptionIndex = null;
127+
if (correct) {
128+
currentQuestionIndex++;
129+
}
68130
});
69131
},
70-
content: selectedOption == null || component.question.options[selectedOption!].correct
71-
? 'Next question'
72-
: 'Try again',
132+
content: switch ((
133+
currentQuestion == null,
134+
currentQuestionIndex == component.questions.length - 1,
135+
selectedOption?.correct,
136+
)) {
137+
// (isComplete, isLast, isCorrect)
138+
(true, _, _) => 'Restart',
139+
(false, _, false) => 'Try again',
140+
(false, false, _) => 'Next question',
141+
(false, true, _) => 'Finish',
142+
},
73143
),
74144
]);
75145
}

0 commit comments

Comments
 (0)