Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb60c76
feat: new block layouts
Sembauke Mar 25, 2025
e980564
refactor: improve block view layout and challenge display logic
Sembauke Mar 25, 2025
7cb97a3
refactor: enhance block view with progress indicator and remove unuse…
Sembauke Mar 27, 2025
f1464a2
refactor: update block view border radius and simplify superblock vie…
Sembauke Mar 27, 2025
51ef07f
refactor: clean up unused imports in block view and enhance certifica…
Sembauke Mar 27, 2025
92c33ce
refactor: simplify block separator logic and remove unused padding me…
Sembauke Mar 27, 2025
ceb62ae
refactor: introduce BlockType and BlockLayout enums and add new grid …
Sembauke Mar 27, 2025
27fdde2
refactor: implement BlockGridView, BlockLinkView, and BlockListView w…
Sembauke Mar 27, 2025
7f6d0c5
refactor: remove unused downloading logic and clean up BlockTemplateV…
Sembauke Mar 27, 2025
b7fb59e
refactor: remove isStepBased property from SuperBlockView and Block m…
Sembauke Mar 27, 2025
a440a44
refactor: update imports in block templates and SuperBlockView
Sembauke Mar 27, 2025
1c7f8d2
refactor: update ListView in BlockListView to include progress indica…
Sembauke Mar 27, 2025
77c09ce
refactor: simplify layout in BlockTemplateView and BlockGridView; adj…
Sembauke Mar 28, 2025
12d2845
refactor: update BlockTemplateView and BlockListView to handle isOpen…
Sembauke Mar 28, 2025
ef7ecde
feat: add challengeDialogue layout and BlockDialogueView
Sembauke Apr 1, 2025
f474837
feat: add RobotoMono font family to dark theme
Sembauke Apr 3, 2025
40bd83e
fix: correct spelling of 'isDownloaded' in BlockDialogueView and Bloc…
Sembauke Apr 3, 2025
355792d
chore: implement Niraj's suggestions
Sembauke Apr 7, 2025
e15190e
Merge branch 'main' into feat/block-layout-v2
Sembauke Apr 7, 2025
dc94324
fix: correct import paths for challenge_tile and update link button c…
Sembauke Apr 8, 2025
db9855c
fix: update button background color in BlockLinkView
Sembauke Apr 8, 2025
449a685
fix: add isOpen functionality to block templates and manage open states
Sembauke Apr 8, 2025
752ab82
fix: stop calling future builder on state update
Sembauke Apr 8, 2025
9e1789b
fix: set the first block to open in block open states
Sembauke Apr 8, 2025
f3aae55
fix: refactor block rendering to use ListView and streamline open/clo…
Sembauke Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 35 additions & 31 deletions mobile-app/lib/models/learn/curriculum_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,38 +46,40 @@ class SuperBlock {
}
}

enum BlockType { lecture, workshop, lab, review, quiz, exam, legacy }

enum BlockLayout {
challengeList,
challengeGrid,
challengeDialogue,
challengeLink,
project,
}

class Block {
final String name;
final String dashedName;
final SuperBlock superBlock;
final BlockLayout layout;
final BlockType type;
final List description;
final bool isStepBased;
final int order;

final List<ChallengeOrder> challenges;
final List<ChallengeListTile> challengeTiles;

Block({
required this.superBlock,
required this.layout,
required this.type,
required this.name,
required this.dashedName,
required this.description,
required this.isStepBased,
required this.order,
required this.challenges,
required this.challengeTiles,
});

static bool checkIfStepBased(String superblock) {
List<String> stepbased = [
'2022/responsive-web-design',
'a2-english-for-developers',
'b1-english-for-developers'
];

return stepbased.contains(superblock);
}

factory Block.fromJson(
Map<String, dynamic> data,
List description,
Expand All @@ -90,18 +92,36 @@ class Block {

data['challengeTiles'] = [];

BlockLayout handleLayout(String? layout) {
switch (layout) {
case 'project-list':
case 'challenge-list':
case 'legacy-challenge-list':
return BlockLayout.challengeList;
case 'dialogue-grid':
return BlockLayout.challengeDialogue;
case 'challenge-grid':
case 'legacy-challenge-grid':
return BlockLayout.challengeGrid;
case 'link':
case 'legacy-link':
return BlockLayout.challengeLink;
default:
return BlockLayout.challengeGrid;
}
}

return Block(
superBlock: SuperBlock(
dashedName: superBlockDashedName,
name: superBlockName,
),
layout: handleLayout(data['blockLayout']),
type: BlockType.legacy,
name: data['name'],
dashedName: key,
description: description,
order: data['order'],
isStepBased: checkIfStepBased(
superBlockDashedName,
),
challenges: (data['challengeOrder'] as List)
.map<ChallengeOrder>(
(dynamic challenge) => ChallengeOrder(
Expand All @@ -125,22 +145,6 @@ class Block {
.toList(),
);
}

static Map<String, dynamic> toCachedObject(Block block) {
return {
'superBlock': {
'dashedName': block.superBlock.dashedName,
'name': block.superBlock.name,
},
'name': block.name,
'dashedName': block.dashedName,
'description': block.description,
'order': block.order,
'isStepBased': block.isStepBased,
'challengeOrder': block.challenges,
'challengeTiles': block.challenges,
};
}
}

class ChallengeListTile {
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/lib/service/learn/learn_offline_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ class LearnOfflineService {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();

Map<String, dynamic> blockToJson = Block.toCachedObject(block);
Map<String, dynamic> blockToJson = {};
List<String>? storedBlocks = prefs.getStringList('storedBlocks');

if (storedBlocks == null) {
Expand Down
90 changes: 90 additions & 0 deletions mobile-app/lib/ui/views/learn/block/block_template_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:freecodecamp/models/learn/curriculum_model.dart';
import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart';
import 'package:stacked/stacked.dart';

class BlockTemplateView extends StatelessWidget {
final Block block;
final bool isOpen;
final Function isOpenFunction;

const BlockTemplateView({
Key? key,
required this.block,
required this.isOpen,
required this.isOpenFunction,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return ViewModelBuilder<BlockTemplateViewModel>.reactive(
onViewModelReady: (model) async {
model.init(block.challengeTiles);
model.setIsDev = await model.developerService.developmentMode();
},
viewModelBuilder: () => BlockTemplateViewModel(),
builder: (
context,
model,
child,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1),
),
color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1),
),
padding: const EdgeInsets.all(8),
width: MediaQuery.of(context).size.width,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
block.name,
style: const TextStyle(
wordSpacing: 0,
letterSpacing: 0,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
Row(
children: [
model.getLayout(
block.layout,
model,
block,
isOpen,
isOpenFunction,
),
],
),
],
),
),
],
),
),
);
},
);
}
}
140 changes: 140 additions & 0 deletions mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:freecodecamp/app/app.locator.dart';
import 'package:freecodecamp/app/app.router.dart';
import 'package:freecodecamp/models/learn/completed_challenge_model.dart';
import 'package:freecodecamp/models/learn/curriculum_model.dart';
import 'package:freecodecamp/models/main/user_model.dart';
import 'package:freecodecamp/service/authentication/authentication_service.dart';
import 'package:freecodecamp/service/developer_service.dart';
import 'package:freecodecamp/service/learn/learn_offline_service.dart';
import 'package:freecodecamp/service/learn/learn_service.dart';
import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_view.dart';
import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart';
import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart';
import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';

class BlockTemplateViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>();
final AuthenticationService _auth = locator<AuthenticationService>();

FccUserModel? user;

bool _isDev = false;
bool get isDev => _isDev;

int _challengesCompleted = 0;
int get challengesCompleted => _challengesCompleted;

final learnOfflineService = locator<LearnOfflineService>();

final developerService = locator<DeveloperService>();

final learnService = locator<LearnService>();

set setIsDev(bool value) {
_isDev = value;
notifyListeners();
}

void routeToChallengeView(Block block, String challengeId) {
_navigationService.navigateTo(
Routes.challengeTemplateView,
arguments: ChallengeTemplateViewArguments(
challengeId: challengeId,
block: block,
challengesCompleted: _challengesCompleted,
),
);
}

Future<void> routeToCertification(Block block) async {
String challengeId = block.challengeTiles[0].id;

routeToChallengeView(
block,
challengeId,
);
}

void init(List<ChallengeListTile> challengeBatch) async {
user = await _auth.userModel;
setNumberOfCompletedChallenges(challengeBatch);
notifyListeners();
}

void setNumberOfCompletedChallenges(List<ChallengeListTile> challengeBatch) {
int count = 0;
if (user != null) {
Iterable<String> completedChallengeIds = user!.completedChallenges.map(
(e) => e.id,
);

for (ChallengeListTile challenge in challengeBatch) {
if (completedChallengeIds.contains(challenge.id)) {
count++;
}
}
_challengesCompleted = count;
notifyListeners();
}
}

bool completedChallenge(String incomingId) {
if (user != null) {
for (CompletedChallenge challenge in user!.completedChallenges) {
if (challenge.id == incomingId) {
return true;
}
}
}

return false;
}

Widget getLayout(
BlockLayout layout,
BlockTemplateViewModel model,
Block block,
bool isOpen,
Function isOpenFunction,
) {
switch (layout) {
case BlockLayout.challengeGrid:
return BlockGridView(
block: block,
model: model,
isOpen: isOpen,
isOpenFunction: isOpenFunction,
);
case BlockLayout.challengeDialogue:
return BlockDialogueView(
block: block,
model: model,
isOpen: isOpen,
isOpenFunction: isOpenFunction,
);
case BlockLayout.challengeList:
return BlockListView(
block: block,
model: model,
isOpen: isOpen,
isOpenFunction: isOpenFunction,
);
case BlockLayout.challengeLink:
return BlockLinkView(
block: block,
model: model,
);
default:
return BlockGridView(
block: block,
model: model,
isOpen: isOpen,
isOpenFunction: isOpenFunction,
);
}
}
}
Loading