Skip to content

Commit 5033527

Browse files
SembaukehuyenltnguyenNirajn2311
authored
chore: merge version 7 branch into main (#1694)
* feat: landing v9 layout (#1684) * refactor: simplify button background color logic and update super block layout handling * Update mobile-app/lib/ui/views/learn/landing/landing_viewmodel.dart Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> --------- Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> * fix: remove exams from chapter block view (#1693) * fix: remove exams from chapters list view * fix: remove log * Merge main into feat/7 * feat: add nodules support for interactive challenges (#1674) * feat: add nodules support for interactive challenges and update pubspec.lock dependencies * fix: make nodule values nullable * fix: multiple files in interactive editor * feat: refactor multiple choice view to use ExampleEditor for nodules --------- Co-authored-by: Niraj Nandish <nirajnandish@icloud.com> * fix: chinese SB * chore: update phone_ide dependency to version 2.0.1 (#1696) * feat: add new icons for SuperBlock fix: add icons to v9 superblocks (#1697) Co-authored-by: Niraj Nandish <nirajnandish@icloud.com> * feat: handle single block modules to directly navigate to challenge and add some extra color (#1695) * feat(chapter-view): enhance module display with dynamic colors and navigation * feat(chapter-view): add visual indicators for certification projects and reviews * feat: add A1 Chinese and A1 Spanish icons (#1698) * feat: add A1 Chinese and A1 Spanish icons * refactor: English icons * refactor: remove A2 Spanish * fix: y value * fix: Spanish Cert issues (#1701) * fix: Spanish cert issues * fix: correct indentation for audio challenge card in review view * fix: remove archived super blocks (#1700) * fix: highlighted text style in spanish lessons --------- Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Co-authored-by: Niraj Nandish <nirajnandish@icloud.com>
1 parent 298aa09 commit 5033527

16 files changed

Lines changed: 402 additions & 102 deletions

File tree

Lines changed: 8 additions & 0 deletions
Loading
Lines changed: 8 additions & 0 deletions
Loading
Lines changed: 6 additions & 2 deletions
Loading
Lines changed: 6 additions & 2 deletions
Loading

mobile-app/lib/models/learn/challenge_model.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ enum HelpCategory {
3030
backend('Backend Development'),
3131
cSharp('C-Sharp'),
3232
english('English'),
33+
chinese('Chinese Curriculum'),
34+
spanish('Spanish Curriculum'),
3335
odin('Odin'),
3436
euler('Euler'),
3537
rosetta('Rosetta');
@@ -68,6 +70,9 @@ class Challenge {
6870
final EnglishAudio? audio;
6971
final Scene? scene;
7072

73+
// Nodules for interactive challenges
74+
final List<Nodules>? nodules;
75+
7176
final List<Question>? questions;
7277

7378
// Challenge Type 15 - Odin
@@ -92,6 +97,7 @@ class Challenge {
9297
required this.helpCategory,
9398
this.explanation,
9499
required this.transcript,
100+
this.nodules,
95101
this.questions,
96102
this.assignments,
97103
this.fillInTheBlank,
@@ -127,6 +133,9 @@ class Challenge {
127133
files: (data['challengeFiles'] ?? [])
128134
.map<ChallengeFile>((file) => ChallengeFile.fromJson(file))
129135
.toList(),
136+
nodules: (data['nodules'] ?? [])
137+
.map<Nodules>((nodule) => Nodules.fromJson(nodule))
138+
.toList(),
130139
questions: (data['questions'] as List).isNotEmpty
131140
? (data['questions'] as List)
132141
.map<Question>((q) => Question.fromJson(q))
@@ -225,6 +234,96 @@ class Hooks {
225234
}
226235
}
227236

237+
sealed class StringOrList {}
238+
239+
enum NoduleType {
240+
paragraph('paragraph'),
241+
interactiveEditor('interactiveEditor');
242+
243+
final String type;
244+
245+
const NoduleType(this.type);
246+
}
247+
248+
class NoduleTypeString extends StringOrList {
249+
final String value;
250+
251+
NoduleTypeString(this.value);
252+
}
253+
254+
class NoduleTypeList extends StringOrList {
255+
final List<InterActiveFile> value;
256+
257+
NoduleTypeList(this.value);
258+
}
259+
260+
class Nodules {
261+
final NoduleType type;
262+
final StringOrList data;
263+
264+
Nodules({
265+
required this.type,
266+
required this.data,
267+
});
268+
269+
String asString() {
270+
return (data as NoduleTypeString).value;
271+
}
272+
273+
List<InterActiveFile> asList() {
274+
return (data as NoduleTypeList).value;
275+
}
276+
277+
factory Nodules.fromJson(Map<String, dynamic> data) {
278+
final noduleType = NoduleType.values.firstWhere(
279+
(type) => type.type == data['type'],
280+
orElse: () => throw ArgumentError('Invalid nodule type: ${data['type']}'),
281+
);
282+
283+
final noduleData = data['data'];
284+
if (noduleType == NoduleType.paragraph) {
285+
return Nodules(
286+
type: noduleType,
287+
data: NoduleTypeString(noduleData),
288+
);
289+
} else if (noduleType == NoduleType.interactiveEditor) {
290+
return Nodules(
291+
type: noduleType,
292+
data: NoduleTypeList(
293+
noduleData
294+
.map<InterActiveFile>(
295+
(item) => InterActiveFile.fromJson(item),
296+
)
297+
.toList(),
298+
),
299+
);
300+
} else {
301+
throw ArgumentError(
302+
'Invalid nodule data type: ${noduleData.runtimeType}');
303+
}
304+
}
305+
}
306+
307+
class InterActiveFile {
308+
final String name;
309+
final String contents;
310+
final String ext;
311+
312+
InterActiveFile({
313+
required this.ext,
314+
required this.name,
315+
required this.contents,
316+
});
317+
318+
factory InterActiveFile.fromJson(Map<String, dynamic> data) {
319+
return InterActiveFile(
320+
ext: data['ext'],
321+
name: data['name'],
322+
contents: data['contents'],
323+
);
324+
}
325+
}
326+
228327
class Question {
229328
final String text;
230329
final List<Answer> answers;

mobile-app/lib/models/learn/curriculum_model.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ enum SuperBlocks {
3030
b1English('b1-english-for-developers'),
3131
a2Spanish('a2-professional-spanish'),
3232
a1Spanish('a1-professional-spanish'),
33-
a2Chinese('a2-professional-chinese'),
33+
a1Chinese('a1-professional-chinese'),
3434
rosettaCode('rosetta-code'),
3535
pythonForEverybody('python-for-everybody'),
3636
devPlayground('dev-playground');
@@ -102,6 +102,7 @@ class SuperBlock {
102102
dashedName: dashedName,
103103
name: name,
104104
blocks: (data[data.keys.first]['blocks']).map<Block>((block) {
105+
105106
return Block.fromJson(
106107
block['meta'],
107108
block['intro'],
@@ -193,6 +194,16 @@ class Block {
193194

194195
data['challengeTiles'] = [];
195196

197+
// Clean up description by filtering out empty or whitespace-only strings
198+
final cleanedDescription = description
199+
.where((item) => item != null && item.toString().trim().isNotEmpty)
200+
.toList();
201+
202+
// If description is empty, provide a default message
203+
final finalDescription = cleanedDescription.isEmpty
204+
? ['Continue to improve your skills']
205+
: cleanedDescription;
206+
196207
BlockLayout handleLayout(String? layout) {
197208
switch (layout) {
198209
case 'project-list':
@@ -228,7 +239,7 @@ class Block {
228239
: BlockLabel.legacy,
229240
name: data['name'],
230241
dashedName: dashedName,
231-
description: description,
242+
description: finalDescription,
232243
order: data['order'],
233244
challenges: (data['challengeOrder'] as List)
234245
.map<ChallengeOrder>(

mobile-app/lib/ui/views/learn/challenge/templates/multiple_choice/multiple_choice_view.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:freecodecamp/ui/views/learn/widgets/quiz_widget.dart';
1212
import 'package:freecodecamp/ui/views/learn/widgets/transcript_widget.dart';
1313
import 'package:freecodecamp/ui/views/learn/widgets/youtube_player_widget.dart';
1414
import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart';
15+
import 'package:phone_ide/phone_ide.dart';
1516
import 'package:stacked/stacked.dart';
1617

1718
class MultipleChoiceView extends StatelessWidget {
@@ -59,6 +60,11 @@ class MultipleChoiceView extends StatelessWidget {
5960
),
6061
),
6162
),
63+
if (challenge.nodules?.isNotEmpty ?? false)
64+
ExampleEditor(
65+
nodules: challenge.nodules!,
66+
parser: parser,
67+
),
6268
if (challenge.videoId != null) ...[
6369
ChallengeCard(
6470
title: 'Watch the Video',
@@ -226,3 +232,71 @@ class MultipleChoiceView extends StatelessWidget {
226232
);
227233
}
228234
}
235+
236+
class ExampleEditor extends StatelessWidget {
237+
const ExampleEditor({
238+
super.key,
239+
required this.nodules,
240+
required this.parser,
241+
});
242+
243+
final List<Nodules> nodules;
244+
final HTMLParser parser;
245+
246+
@override
247+
Widget build(BuildContext context) {
248+
return ChallengeCard(
249+
title: 'Lesson',
250+
child: Column(
251+
children: [
252+
...nodules.map(
253+
(nodule) {
254+
if (nodule.type == NoduleType.paragraph) {
255+
return Column(
256+
children: parser.parse(nodule.asString()),
257+
);
258+
} else if (nodule.type == NoduleType.interactiveEditor) {
259+
return Column(
260+
children: nodule
261+
.asList()
262+
.map(
263+
(file) => Padding(
264+
padding: const EdgeInsets.only(bottom: 16.0),
265+
child: Column(
266+
crossAxisAlignment: CrossAxisAlignment.start,
267+
children: [
268+
Text(
269+
file.ext.toUpperCase(),
270+
style: const TextStyle(
271+
fontWeight: FontWeight.bold,
272+
fontSize: 16,
273+
),
274+
),
275+
const SizedBox(height: 8),
276+
Editor(
277+
options: EditorOptions(
278+
fontFamily: 'Hack',
279+
takeFullHeight: false,
280+
showLinebar: false,
281+
isEditable: false,
282+
),
283+
defaultLanguage: file.ext,
284+
defaultValue: file.contents,
285+
path: '/',
286+
),
287+
],
288+
),
289+
),
290+
)
291+
.toList(),
292+
);
293+
} else {
294+
return const SizedBox.shrink();
295+
}
296+
},
297+
)
298+
],
299+
),
300+
);
301+
}
302+
}

mobile-app/lib/ui/views/learn/challenge/templates/review/review_view.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:freecodecamp/ui/theme/fcc_theme.dart';
66
import 'package:freecodecamp/ui/views/learn/challenge/templates/review/review_viewmodel.dart';
77
import 'package:freecodecamp/ui/views/learn/utils/challenge_utils.dart';
88
import 'package:freecodecamp/ui/views/learn/widgets/assignment_widget.dart';
9+
import 'package:freecodecamp/ui/views/learn/widgets/audio/audio_player_view.dart';
910
import 'package:freecodecamp/ui/views/learn/widgets/challenge_card.dart';
1011
import 'package:freecodecamp/ui/views/learn/widgets/transcript_widget.dart';
1112
import 'package:freecodecamp/ui/views/learn/widgets/youtube_player_widget.dart';
@@ -55,6 +56,14 @@ class ReviewView extends StatelessWidget {
5556
),
5657
const SizedBox(height: 12),
5758
],
59+
if (challenge.audio != null) ...[
60+
ChallengeCard(
61+
title: 'Listen to the Audio',
62+
child: AudioPlayerView(
63+
audio: challenge.audio!,
64+
),
65+
),
66+
],
5867
if (challenge.transcript.isNotEmpty) ...[
5968
ChallengeCard(
6069
title: 'Transcript',

0 commit comments

Comments
 (0)