Skip to content

Commit 3ac390c

Browse files
committed
add quiz component
1 parent fa58364 commit 3ac390c

File tree

9 files changed

+302
-20
lines changed

9 files changed

+302
-20
lines changed

site/lib/_sass/_site.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
@use 'components/next-prev-nav';
3131
@use 'components/os-selector';
3232
@use 'components/pill';
33+
@use 'components/quiz';
3334
@use 'components/sidebar';
3435
@use 'components/side-menu';
3536
@use 'components/site-switcher';
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
.quiz {
2+
3+
4+
ol {
5+
padding: 0;
6+
margin: 0;
7+
margin-top: 1rem;
8+
list-style: upper-alpha;
9+
list-style-position: inside;
10+
11+
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;
18+
19+
&:not(:where(.selected, .disabled)):hover {
20+
background-color: var(--site-inset-bgColor);
21+
cursor: pointer;
22+
}
23+
24+
&.selected:has(.correct) {
25+
background-color: oklch(from var(--site-alert-tip-color) l c h / 0.2);
26+
}
27+
28+
&.selected:has(.incorrect) {
29+
background-color: oklch(from var(--site-alert-error-color) l c h / 0.2);
30+
}
31+
32+
&.disabled {
33+
opacity: 0.6;
34+
}
35+
36+
p {
37+
margin-bottom: 0;
38+
}
39+
40+
.question-wrapper {
41+
display: grid;
42+
grid-template-rows: min-content 0fr;
43+
transition: grid-template-rows 500ms;
44+
}
45+
46+
&.selected .question-wrapper {
47+
grid-template-rows: min-content 1fr;
48+
}
49+
50+
.question {
51+
margin-top: -1lh;
52+
margin-left: 1.4rem;
53+
}
54+
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+
}
71+
}
72+
73+
p.correct {
74+
color: green;
75+
76+
&::before {
77+
content: "";
78+
}
79+
}
80+
81+
p.incorrect {
82+
color: red;
83+
84+
&::before {
85+
content: "";
86+
}
87+
}
88+
}
89+
}
90+
}
91+
92+
93+
}

site/lib/jaspr_options.dart

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@ import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.d
2121
as prefix6;
2222
import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart'
2323
as prefix7;
24-
import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart'
24+
import 'package:docs_flutter_dev_site/src/components/fwe/client/quiz.dart'
2525
as prefix8;
26-
import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart'
26+
import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart'
2727
as prefix9;
28-
import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart'
28+
import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart'
2929
as prefix10;
30-
import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart'
30+
import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart'
3131
as prefix11;
32-
import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart'
32+
import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart'
3333
as prefix12;
34-
import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters.dart'
34+
import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart'
3535
as prefix13;
36-
import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart'
36+
import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters.dart'
3737
as prefix14;
38+
import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart'
39+
as prefix15;
3840

3941
/// Default [JasprOptions] for use with your jaspr project.
4042
///
@@ -90,35 +92,40 @@ JasprOptions get defaultJasprOptions => JasprOptions(
9092
params: _prefix7DartPadInjector,
9193
),
9294

93-
prefix8.MenuToggle: ClientTarget<prefix8.MenuToggle>(
95+
prefix8.InteractiveQuiz: ClientTarget<prefix8.InteractiveQuiz>(
96+
'src/components/fwe/client/quiz',
97+
params: _prefix8InteractiveQuiz,
98+
),
99+
100+
prefix9.MenuToggle: ClientTarget<prefix9.MenuToggle>(
94101
'src/components/layout/menu_toggle',
95102
),
96103

97-
prefix9.SiteSwitcher: ClientTarget<prefix9.SiteSwitcher>(
104+
prefix10.SiteSwitcher: ClientTarget<prefix10.SiteSwitcher>(
98105
'src/components/layout/site_switcher',
99106
),
100107

101-
prefix10.ThemeSwitcher: ClientTarget<prefix10.ThemeSwitcher>(
108+
prefix11.ThemeSwitcher: ClientTarget<prefix11.ThemeSwitcher>(
102109
'src/components/layout/theme_switcher',
103110
),
104111

105-
prefix11.ArchiveTable: ClientTarget<prefix11.ArchiveTable>(
112+
prefix12.ArchiveTable: ClientTarget<prefix12.ArchiveTable>(
106113
'src/components/pages/archive_table',
107-
params: _prefix11ArchiveTable,
114+
params: _prefix12ArchiveTable,
108115
),
109116

110-
prefix12.GlossarySearchSection:
111-
ClientTarget<prefix12.GlossarySearchSection>(
117+
prefix13.GlossarySearchSection:
118+
ClientTarget<prefix13.GlossarySearchSection>(
112119
'src/components/pages/glossary_search_section',
113120
),
114121

115-
prefix13.LearningResourceFilters:
116-
ClientTarget<prefix13.LearningResourceFilters>(
122+
prefix14.LearningResourceFilters:
123+
ClientTarget<prefix14.LearningResourceFilters>(
117124
'src/components/pages/learning_resource_filters',
118125
),
119126

120-
prefix14.LearningResourceFiltersSidebar:
121-
ClientTarget<prefix14.LearningResourceFiltersSidebar>(
127+
prefix15.LearningResourceFiltersSidebar:
128+
ClientTarget<prefix15.LearningResourceFiltersSidebar>(
122129
'src/components/pages/learning_resource_filters_sidebar',
123130
),
124131
},
@@ -143,7 +150,10 @@ Map<String, dynamic> _prefix7DartPadInjector(prefix7.DartPadInjector c) => {
143150
'height': c.height,
144151
'runAutomatically': c.runAutomatically,
145152
};
146-
Map<String, dynamic> _prefix11ArchiveTable(prefix11.ArchiveTable c) => {
153+
Map<String, dynamic> _prefix8InteractiveQuiz(prefix8.InteractiveQuiz c) => {
154+
'question': c.question.toJson(),
155+
};
156+
Map<String, dynamic> _prefix12ArchiveTable(prefix12.ArchiveTable c) => {
147157
'os': c.os,
148158
'channel': c.channel,
149159
};

site/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'src/components/common/client/os_selector.dart';
1414
import 'src/components/common/dash_image.dart';
1515
import 'src/components/common/tabs.dart';
1616
import 'src/components/common/youtube_embed.dart';
17+
import 'src/components/fwe/quiz.dart';
1718
import 'src/components/pages/archive_table.dart';
1819
import 'src/components/pages/devtools_release_notes_index.dart';
1920
import 'src/components/pages/expansion_list.dart';
@@ -75,6 +76,7 @@ List<CustomComponent> get _embeddableComponents => [
7576
const DashTabs(),
7677
const DashImage(),
7778
const YoutubeEmbed(),
79+
const Quiz(),
7880
CustomComponent(
7981
pattern: RegExp('OSSelector', caseSensitive: false),
8082
builder: (_, _, _) => const OsSelector(),
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
7+
import '../../../util.dart';
8+
9+
@client
10+
class InteractiveQuiz extends StatefulComponent {
11+
const InteractiveQuiz({required this.question, super.key});
12+
13+
final Question question;
14+
15+
@override
16+
State<InteractiveQuiz> createState() => _InteractiveQuizState();
17+
}
18+
19+
class _InteractiveQuizState extends State<InteractiveQuiz> {
20+
int? selectedOption;
21+
22+
@override
23+
Component build(BuildContext context) {
24+
return div([
25+
strong([text(component.question.question)]),
26+
ol([
27+
for (final (index, option) in component.question.options.indexed)
28+
li(
29+
classes: [
30+
if (selectedOption != null)
31+
if (selectedOption == index) 'selected' else 'disabled',
32+
].toClasses,
33+
events: {
34+
'click': (_) {
35+
if (selectedOption != null) {
36+
return;
37+
}
38+
setState(() {
39+
selectedOption = index;
40+
});
41+
},
42+
},
43+
[
44+
div(classes: 'question-wrapper', [
45+
div(classes: 'question', [
46+
p([text(option.text)]),
47+
]),
48+
div(classes: 'solution', [
49+
if (option.correct)
50+
p(classes: 'correct', [text('That\'s right!')])
51+
else
52+
p(classes: 'incorrect', [text('Not quite')]),
53+
p([text(option.explanation)]),
54+
]),
55+
]),
56+
],
57+
),
58+
]),
59+
]);
60+
}
61+
}
62+
63+
class Question {
64+
const Question(this.question, this.options);
65+
66+
final String question;
67+
final List<AnswerOption> options;
68+
69+
@decoder
70+
factory Question.fromMap(Map<Object?, Object?> json) {
71+
return Question(
72+
json['question'] as String,
73+
(json['options'] as List<Object?>)
74+
.map((e) => AnswerOption.fromJson(e as Map<Object?, Object?>))
75+
.toList(),
76+
);
77+
}
78+
79+
@encoder
80+
Map<Object?, Object?> toJson() => {
81+
'question': question,
82+
'options': options.map((e) => e.toJson()).toList(),
83+
};
84+
}
85+
86+
class AnswerOption {
87+
const AnswerOption(this.text, this.correct, this.explanation);
88+
89+
final String text;
90+
final bool correct;
91+
final String explanation;
92+
93+
@decoder
94+
factory AnswerOption.fromJson(Map<Object?, Object?> json) {
95+
return AnswerOption(
96+
json['text'] as String,
97+
json['correct'] as bool? ?? false,
98+
json['explanation'] as String,
99+
);
100+
}
101+
102+
@encoder
103+
Map<Object?, Object?> toJson() => {
104+
'text': text,
105+
'correct': correct,
106+
'explanation': explanation,
107+
};
108+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
7+
import 'package:yaml/yaml.dart';
8+
9+
import 'client/quiz.dart';
10+
11+
class Quiz implements CustomComponent {
12+
const Quiz();
13+
14+
@override
15+
Component? create(Node node, NodesBuilder builder) {
16+
if (node is ElementNode && node.tag.toLowerCase() == 'quiz') {
17+
if (node.children?.whereType<ElementNode>().isNotEmpty ?? false) {
18+
throw Exception(
19+
'Invalid Quiz content. Remove any leading empty lines to '
20+
'avoid parsing as markdown.',
21+
);
22+
}
23+
24+
final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
25+
final data = loadYamlNode(content);
26+
assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.');
27+
final questions = (data as YamlList).nodes
28+
.map((n) => Question.fromMap(n as YamlMap))
29+
.toList();
30+
return div(classes: 'quiz not-content', [
31+
for (final question in questions) InteractiveQuiz(question: question),
32+
]);
33+
}
34+
return null;
35+
}
36+
}

0 commit comments

Comments
 (0)