feat: courses repo (#3777)

pull/2245/head
Wilson 2 months ago committed by GitHub
parent 4e3f82331c
commit a62d9f8643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -220,7 +220,7 @@ class Vocab {
class ActivityRole {
final String id;
final String name;
final String goal;
final String? goal;
final String? avatarUrl;
ActivityRole({
@ -238,9 +238,9 @@ class ActivityRole {
}
return ActivityRole(
id: json['id'],
name: json['name'],
goal: json['goal'],
id: json['id'] as String,
name: json['name'] as String,
goal: json['goal'] as String?,
avatarUrl: avatarUrl,
);
}

@ -14,8 +14,8 @@ import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/courses/course_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -38,7 +38,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
final courseParent = controller.room.courseParent;
if (courseParent?.coursePlan == null) return;
final coursePlan = await CourseRepo.get(
final coursePlan = await CoursePlansRepo.get(
courseParent!.coursePlan!.uuid,
);

@ -15,7 +15,7 @@ import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';

@ -17,9 +17,9 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_settings/course_settings.dart';
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';

@ -64,6 +64,14 @@ class Environment {
return envEntry;
}
static String get cmsApi {
final envEntry = appConfigOverride?.choreoApi ?? dotenv.env['CHOREO_API'];
if (envEntry == null) {
return "Not found";
}
return envEntry;
}
static String get choreoApiKey {
return appConfigOverride?.choreoApiKey ??
dotenv.env['CHOREO_API_KEY'] ??

@ -14,8 +14,8 @@ import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_chats/course_chats_view.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';

@ -12,9 +12,9 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart';
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/space_analytics/analytics_request_indicator.dart';
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';

@ -0,0 +1,51 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:fluffychat/widgets/matrix.dart';
class CourseImage extends StatelessWidget {
final String? imageUrl;
final double width;
final Widget? replacement;
final BorderRadius borderRadius;
const CourseImage({
super.key,
required this.imageUrl,
required this.width,
this.replacement,
this.borderRadius = const BorderRadius.all(Radius.circular(20.0)),
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: borderRadius,
child: imageUrl != null
? CachedNetworkImage(
width: width,
height: width,
fit: BoxFit.cover,
imageUrl: imageUrl!,
httpHeaders: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return replacement ?? const SizedBox();
},
)
: replacement ?? const SizedBox(),
);
}
}

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
class CourseInfoChip extends StatelessWidget {

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
// ignore_for_file: depend_on_referenced_packages
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/course_creation/course_image_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
class CoursePlanTile extends StatelessWidget {
@ -40,36 +41,16 @@ class CoursePlanTile extends StatelessWidget {
child: Row(
spacing: 4.0,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: course.imageUrl != null
? CachedNetworkImage(
width: 40.0,
height: 40.0,
fit: BoxFit.cover,
imageUrl: course.imageUrl!,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
);
},
)
: Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
CourseImage(
imageUrl: course.imageUrl,
width: 40.0,
replacement: Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
Flexible(
child: Column(

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/course_creation/new_course_view.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/course_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
@ -55,7 +55,7 @@ class NewCourseController extends State<NewCourse> {
Future<void> _loadCourses() async {
try {
setState(() => loading = true);
courses = await CourseRepo.search(filter: _filter);
courses = await CoursePlansRepo.search(filter: _filter);
} catch (e) {
error = e;
} finally {

@ -9,7 +9,7 @@ import 'package:matrix/matrix.dart' as sdk;
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/course_creation/selected_course_view.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -29,7 +29,13 @@ class SelectedCourseController extends State<SelectedCourse> {
Uri? avatarUrl;
if (course.imageUrl != null) {
try {
final Response response = await http.get(Uri.parse(course.imageUrl!));
final Response response = await http.get(
Uri.parse(course.imageUrl!),
headers: {
'Authorization':
'Bearer ${MatrixState.pangeaController.userController.accessToken}',
},
);
avatar = response.bodyBytes;
avatarUrl = await client.uploadContent(avatar);
} catch (e) {

@ -4,9 +4,10 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/course_creation/course_image_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@ -57,39 +58,16 @@ class SelectedCourseView extends StatelessWidget {
return Column(
spacing: 8.0,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: course.imageUrl != null
? CachedNetworkImage(
width: 100.0,
height: 100.0,
fit: BoxFit.cover,
imageUrl: course.imageUrl!,
placeholder: (context, url) {
return const Center(
child:
CircularProgressIndicator(),
);
},
errorWidget: (context, url, error) {
return Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
color: theme
.colorScheme.secondary,
),
);
},
)
: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
color:
theme.colorScheme.secondary,
),
),
CourseImage(
imageUrl: course.imageUrl,
width: 100.0,
replacement: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
Text(
course.title,

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/course_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart';
class CoursePlanBuilder extends StatefulWidget {
final String? courseId;
@ -60,7 +60,7 @@ class CoursePlanController extends State<CoursePlanBuilder> {
error = null;
});
course = await CourseRepo.get(widget.courseId!);
course = await CoursePlansRepo.get(widget.courseId!);
course == null
? widget.onNotFound?.call()
: widget.onFound?.call(course!);

@ -0,0 +1,267 @@
import 'package:fluffychat/pangea/activity_generator/media_enum.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart';
/// Represents a topic in the course planner response.
class Topic {
final String title;
final String description;
final String location;
final String uuid;
final String? imageUrl;
final List<ActivityPlanModel> activities;
Topic({
required this.title,
required this.description,
this.location = "Unknown",
required this.uuid,
List<ActivityPlanModel>? activities,
this.imageUrl,
}) : activities = activities ?? [];
/// Deserialize from JSON
factory Topic.fromJson(Map<String, dynamic> json) {
return Topic(
title: json['title'] as String,
description: json['description'] as String,
location: json['location'] as String? ?? "Unknown",
uuid: json['uuid'] as String,
activities: (json['activities'] as List<dynamic>?)
?.map(
(e) => ActivityPlanModel.fromJson(e as Map<String, dynamic>),
)
.toList() ??
[],
imageUrl: json['image_url'] as String?,
);
}
/// Serialize to JSON
Map<String, dynamic> toJson() {
return {
'title': title,
'description': description,
'location': location,
'uuid': uuid,
'activities': activities.map((e) => e.toJson()).toList(),
'image_url': imageUrl,
};
}
List<String> get activityIds => activities.map((e) => e.activityId).toList();
}
/// Represents a course plan in the course planner response.
class CoursePlanModel {
final String targetLanguage;
final String languageOfInstructions;
final LanguageLevelTypeEnum cefrLevel;
final String title;
final String description;
final String uuid;
final List<Topic> topics;
final String? imageUrl;
CoursePlanModel({
required this.targetLanguage,
required this.languageOfInstructions,
required this.cefrLevel,
required this.title,
required this.description,
required this.uuid,
List<Topic>? topics,
this.imageUrl,
}) : topics = topics ?? [];
int get activities =>
topics.map((t) => t.activities.length).reduce((a, b) => a + b);
LanguageModel? get targetLanguageModel =>
PLanguageStore.byLangCode(targetLanguage);
LanguageModel? get baseLanguageModel =>
PLanguageStore.byLangCode(languageOfInstructions);
String get targetLanguageDisplay =>
targetLanguageModel?.langCode.toUpperCase() ??
targetLanguage.toUpperCase();
String get baseLanguageDisplay =>
baseLanguageModel?.langCode.toUpperCase() ??
languageOfInstructions.toUpperCase();
String? topicID(String activityID) {
for (final topic in topics) {
for (final activity in topic.activities) {
if (activity.activityId == activityID) {
return topic.uuid;
}
}
}
return null;
}
/// Deserialize from JSON
factory CoursePlanModel.fromJson(Map<String, dynamic> json) {
return CoursePlanModel(
targetLanguage: json['target_language'] as String,
languageOfInstructions: json['language_of_instructions'] as String,
cefrLevel: LanguageLevelTypeEnumExtension.fromString(json['cefr_level']),
title: json['title'] as String,
description: json['description'] as String,
uuid: json['uuid'] as String,
topics: (json['topics'] as List<dynamic>?)
?.map((e) => Topic.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
imageUrl: json['image_url'] as String?,
);
}
/// Serialize to JSON
Map<String, dynamic> toJson() {
return {
'target_language': targetLanguage,
'language_of_instructions': languageOfInstructions,
'cefr_level': cefrLevel.string,
'title': title,
'description': description,
'uuid': uuid,
'topics': topics.map((e) => e.toJson()).toList(),
'image_url': imageUrl,
};
}
factory CoursePlanModel.fromCmsDocs(
CmsCoursePlan cmsCoursePlan,
List<CmsCoursePlanMedia>? cmsCoursePlanMedias,
List<CmsCoursePlanModule>? cmsCoursePlanModules,
List<CmsCoursePlanModuleLocation>? cmsCoursePlanModuleLocations,
List<CmsCoursePlanActivity>? cmsCoursePlanActivities,
List<CmsCoursePlanActivityMedia>? cmsCoursePlanActivityMedias,
) {
// fetch topics
List<Topic>? topics;
if (cmsCoursePlanModules != null) {
for (final module in cmsCoursePlanModules) {
// select locations of current module
List<CmsCoursePlanModuleLocation>? moduleLocations;
if (cmsCoursePlanModuleLocations != null) {
for (final location in cmsCoursePlanModuleLocations) {
if (location.coursePlanModules.contains(module.id)) {
moduleLocations ??= [];
moduleLocations.add(location);
}
}
}
// select activities of current module
List<CmsCoursePlanActivity>? moduleActivities;
if (cmsCoursePlanActivities != null) {
for (final activity in cmsCoursePlanActivities) {
if (activity.coursePlanModules.contains(module.id)) {
moduleActivities ??= [];
moduleActivities.add(activity);
}
}
}
List<ActivityPlanModel>? activityPlans;
if (moduleActivities != null) {
for (final activity in moduleActivities) {
// select media of current activity
List<CmsCoursePlanActivityMedia>? activityMedias;
if (cmsCoursePlanActivityMedias != null) {
for (final media in cmsCoursePlanActivityMedias) {
if (media.coursePlanActivities.contains(activity.id)) {
activityMedias ??= [];
activityMedias.add(media);
}
}
}
activityPlans ??= [];
activityPlans.add(
ActivityPlanModel(
req: ActivityPlanRequest(
topic: "",
mode: "",
objective: "",
media: MediaEnum.nan,
cefrLevel: activity.cefrLevel,
languageOfInstructions: activity.l1,
targetLanguage: activity.l2,
numberOfParticipants: activity.roles.length,
),
activityId: activity.id,
title: activity.title,
description: activity.description,
learningObjective: activity.learningObjective,
instructions: activity.instructions,
vocab: activity.vocabs
.map((v) => Vocab(lemma: v.lemma, pos: v.pos))
.toList(),
roles: activity.roles.asMap().map(
(index, v) => MapEntry(
index.toString(),
ActivityRole(
id: v.id,
name: v.name,
avatarUrl: v.avatarUrl,
goal: v.goal,
),
),
),
imageURL: activityMedias != null && activityMedias.isNotEmpty
? '${Environment.cmsApi}${activityMedias.first.url}'
: null,
),
);
}
}
topics ??= [];
topics.add(
Topic(
uuid: module.id,
title: module.title,
description: module.description,
location: moduleLocations != null && moduleLocations.isNotEmpty
? moduleLocations.first.name
: "Any",
activities: activityPlans,
),
);
}
}
return CoursePlanModel(
uuid: cmsCoursePlan.id,
title: cmsCoursePlan.title,
description: cmsCoursePlan.description,
cefrLevel:
LanguageLevelTypeEnumExtension.fromString(cmsCoursePlan.cefrLevel),
languageOfInstructions: cmsCoursePlan.l1,
targetLanguage: cmsCoursePlan.l2,
topics: topics,
imageUrl: cmsCoursePlanMedias != null && cmsCoursePlanMedias.isNotEmpty
? '${Environment.cmsApi}${cmsCoursePlanMedias.first.url}'
: null,
);
}
}

@ -1,9 +1,9 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/courses/course_plan_event.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/course_user_event.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_event.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/course_user_event.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
extension CoursePlanRoomExtension on Room {

@ -0,0 +1,333 @@
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_model.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module.dart';
import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart';
import 'package:fluffychat/pangea/payload_client/payload_client.dart';
import 'package:fluffychat/widgets/matrix.dart';
class CourseFilter {
final LanguageModel? targetLanguage;
final LanguageModel? languageOfInstructions;
final LanguageLevelTypeEnum? cefrLevel;
CourseFilter({
this.targetLanguage,
this.languageOfInstructions,
this.cefrLevel,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CourseFilter &&
other.targetLanguage == targetLanguage &&
other.languageOfInstructions == languageOfInstructions &&
other.cefrLevel == cefrLevel;
}
@override
int get hashCode =>
targetLanguage.hashCode ^
languageOfInstructions.hashCode ^
cefrLevel.hashCode;
}
class CoursePlansRepo {
static final GetStorage _courseStorage = GetStorage("course_storage");
static final PayloadClient payload = PayloadClient(
baseUrl: Environment.cmsApi,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
static CoursePlanModel? _getCached(String id) {
final json = _courseStorage.read(id);
if (json != null) {
try {
return CoursePlanModel.fromJson(json);
} catch (e) {
_courseStorage.remove(id);
}
}
return null;
}
static Future<void> _setCached(CoursePlanModel coursePlan) async {
await _courseStorage.write(coursePlan.uuid, coursePlan.toJson());
}
static String _searchKey(CourseFilter filter) {
return "search_${filter.hashCode.toString()}";
}
static List<CoursePlanModel>? _getCachedSearchResults(
CourseFilter filter,
) {
final jsonList = _courseStorage.read(_searchKey(filter));
if (jsonList != null) {
try {
final ids = List<String>.from(jsonList);
final coursePlans = ids
.map((id) => _getCached(id))
.whereType<CoursePlanModel>()
.toList();
return coursePlans;
} catch (e) {
_courseStorage.remove(_searchKey(filter));
}
}
return null;
}
static Future<void> _setCachedSearchResults(
CourseFilter filter,
List<CoursePlanModel> coursePlans,
) async {
final jsonList = coursePlans.map((e) => e.uuid).toList();
for (final plan in coursePlans) {
_setCached(plan);
}
await _courseStorage.write(_searchKey(filter), jsonList);
}
static Future<CoursePlanModel?> get(String id) async {
final cached = _getCached(id);
if (cached != null) {
return cached;
}
final cmsCoursePlan = await payload.findById(
"course-plans",
id,
CmsCoursePlan.fromJson,
);
final coursePlan = await _fromCmsCoursePlan(cmsCoursePlan);
await _setCached(coursePlan);
return coursePlan;
}
static Future<List<CoursePlanModel>> search({CourseFilter? filter}) async {
final cached = _getCachedSearchResults(filter ?? CourseFilter());
if (cached != null && cached.isNotEmpty) {
return cached;
}
final Map<String, dynamic> where = {};
if (filter != null) {
int numberOfFilter = 0;
if (filter.cefrLevel != null) {
numberOfFilter += 1;
}
if (filter.languageOfInstructions != null) {
numberOfFilter += 1;
}
if (filter.targetLanguage != null) {
numberOfFilter += 1;
}
if (numberOfFilter > 1) {
where["and"] = [];
if (filter.cefrLevel != null) {
where["and"].add({
"cefrLevel": {"equals": filter.cefrLevel!.string},
});
}
if (filter.languageOfInstructions != null) {
where["and"].add({
"languageOfInstructions": {
"equals": filter.languageOfInstructions!.langCode,
},
});
}
if (filter.targetLanguage != null) {
where["and"].add({
"targetLanguage": {"equals": filter.targetLanguage!.langCode},
});
}
} else if (numberOfFilter == 1) {
if (filter.cefrLevel != null) {
where["cefrLevel"] = {"equals": filter.cefrLevel!.string};
}
if (filter.languageOfInstructions != null) {
where["languageOfInstructions"] = {
"equals": filter.languageOfInstructions!.langCode,
};
}
if (filter.targetLanguage != null) {
where["targetLanguage"] = {"equals": filter.targetLanguage!.langCode};
}
}
}
final result = await payload.find(
"course-plans",
CmsCoursePlan.fromJson,
page: 1,
limit: 10,
where: where,
);
final coursePlans = await Future.wait(
result.docs.map(
(cmsCoursePlan) => _fromCmsCoursePlan(
cmsCoursePlan,
),
),
);
await _setCachedSearchResults(
filter ?? CourseFilter(),
coursePlans,
);
return coursePlans;
}
static Future<CoursePlanModel> _fromCmsCoursePlan(
CmsCoursePlan cmsCoursePlan,
) async {
final medias = await _getMedia(cmsCoursePlan);
final modules = await _getModules(cmsCoursePlan);
final locations = await _getModuleLocations(modules ?? []);
final activities = await _getModuleActivities(modules ?? []);
final activityMedias = await _getActivityMedia(activities ?? []);
return CoursePlanModel.fromCmsDocs(
cmsCoursePlan,
medias,
modules,
locations,
activities,
activityMedias,
);
}
static Future<List<CmsCoursePlanMedia>?> _getMedia(
CmsCoursePlan cmsCoursePlan,
) async {
final docs = cmsCoursePlan.coursePlanMedia?.docs;
if (docs == null || docs.isEmpty) return null;
final where = {
"id": {"in": docs.join(",")},
};
final limit = docs.length;
final cmsCoursePlanMediaResult = await payload.find(
"course-plan-media",
CmsCoursePlanMedia.fromJson,
where: where,
limit: limit,
page: 1,
sort: "createdAt",
);
return cmsCoursePlanMediaResult.docs;
}
static Future<List<CmsCoursePlanModule>?> _getModules(
CmsCoursePlan cmsCoursePlan,
) async {
final docs = cmsCoursePlan.coursePlanModules?.docs;
if (docs == null || docs.isEmpty) return null;
final where = {
"id": {"in": docs.join(",")},
};
final limit = docs.length;
final cmsCourseModulesResult = await payload.find(
"course-plan-modules",
CmsCoursePlanModule.fromJson,
where: where,
limit: limit,
page: 1,
sort: "createdAt",
);
return cmsCourseModulesResult.docs;
}
static Future<List<CmsCoursePlanModuleLocation>?> _getModuleLocations(
List<CmsCoursePlanModule> modules,
) async {
final List<String> locations = [];
for (final module in modules) {
if (module.coursePlanModuleLocations?.docs != null) {
locations.addAll(module.coursePlanModuleLocations!.docs!);
}
}
if (locations.isEmpty) return null;
final where = {
"id": {"in": locations.join(",")},
};
final limit = locations.length;
final cmsCoursePlanModuleLocationsResult = await payload.find(
"course-plan-module-locations",
CmsCoursePlanModuleLocation.fromJson,
where: where,
limit: limit,
page: 1,
sort: "createdAt",
);
return cmsCoursePlanModuleLocationsResult.docs;
}
static Future<List<CmsCoursePlanActivity>?> _getModuleActivities(
List<CmsCoursePlanModule> module,
) async {
final List<String> activities = [];
for (final mod in module) {
if (mod.coursePlanActivities?.docs != null) {
activities.addAll(mod.coursePlanActivities!.docs!);
}
}
if (activities.isEmpty) return null;
final where = {
"id": {"in": activities.join(",")},
};
final limit = activities.length;
final cmsCoursePlanActivitiesResult = await payload.find(
"course-plan-activities",
CmsCoursePlanActivity.fromJson,
where: where,
limit: limit,
page: 1,
sort: "createdAt",
);
return cmsCoursePlanActivitiesResult.docs;
}
static Future<List<CmsCoursePlanActivityMedia>?> _getActivityMedia(
List<CmsCoursePlanActivity> activity,
) async {
final List<String> mediaIds = [];
for (final act in activity) {
if (act.coursePlanActivityMedia?.docs != null) {
mediaIds.addAll(act.coursePlanActivityMedia!.docs!);
}
}
if (mediaIds.isEmpty) return null;
final where = {
"id": {"in": mediaIds.join(",")},
};
final limit = mediaIds.length;
final cmsCoursePlanActivityMediasResult = await payload.find(
"course-plan-activity-medias",
CmsCoursePlanActivityMedia.fromJson,
where: where,
limit: limit,
page: 1,
sort: "createdAt",
);
return cmsCoursePlanActivityMediasResult.docs;
}
}

@ -11,10 +11,10 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/course_settings/pin_clipper.dart';
import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart';
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
class CourseSettings extends StatelessWidget {
final Room room;

@ -1,140 +0,0 @@
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
/// Represents a topic in the course planner response.
class Topic {
final String title;
final String description;
final String location;
final String uuid;
final String? imageUrl;
final List<ActivityPlanModel> activities;
Topic({
required this.title,
required this.description,
this.location = "Unknown",
required this.uuid,
List<ActivityPlanModel>? activities,
this.imageUrl,
}) : activities = activities ?? [];
/// Deserialize from JSON
factory Topic.fromJson(Map<String, dynamic> json) {
return Topic(
title: json['title'] as String,
description: json['description'] as String,
location: json['location'] as String? ?? "Unknown",
uuid: json['id'] as String,
activities: (json['activities'] as List<dynamic>?)
?.map(
(e) => ActivityPlanModel.fromJson(e as Map<String, dynamic>),
)
.toList() ??
[],
imageUrl: json['image_url'] as String?,
);
}
/// Serialize to JSON
Map<String, dynamic> toJson() {
return {
'title': title,
'description': description,
'location': location,
'id': uuid,
'activities': activities.map((e) => e.toJson()).toList(),
'image_url': imageUrl,
};
}
List<String> get activityIds => activities.map((e) => e.activityId).toList();
}
/// Represents a course plan in the course planner response.
class CoursePlanModel {
final String targetLanguage;
final String languageOfInstructions;
final LanguageLevelTypeEnum cefrLevel;
final String title;
final String description;
final String uuid;
final List<Topic> topics;
final String? imageUrl;
CoursePlanModel({
required this.targetLanguage,
required this.languageOfInstructions,
required this.cefrLevel,
required this.title,
required this.description,
required this.uuid,
List<Topic>? topics,
this.imageUrl,
}) : topics = topics ?? [];
int get activities =>
topics.map((t) => t.activities.length).reduce((a, b) => a + b);
LanguageModel? get targetLanguageModel =>
PLanguageStore.byLangCode(targetLanguage);
LanguageModel? get baseLanguageModel =>
PLanguageStore.byLangCode(languageOfInstructions);
String get targetLanguageDisplay =>
targetLanguageModel?.langCode.toUpperCase() ??
targetLanguage.toUpperCase();
String get baseLanguageDisplay =>
baseLanguageModel?.langCode.toUpperCase() ??
languageOfInstructions.toUpperCase();
String? topicID(String activityID) {
for (final topic in topics) {
for (final activity in topic.activities) {
if (activity.activityId == activityID) {
return topic.uuid;
}
}
}
return null;
}
/// Deserialize from JSON
factory CoursePlanModel.fromJson(Map<String, dynamic> json) {
return CoursePlanModel(
targetLanguage: json['target_language'] as String,
languageOfInstructions: json['language_of_instructions'] as String,
cefrLevel: LanguageLevelTypeEnumExtension.fromString(json['cefr_level']),
title: json['title'] as String,
description: json['description'] as String,
uuid: json['id'] as String,
topics: (json['topics'] as List<dynamic>?)
?.map((e) => Topic.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
imageUrl: json['image_url'] as String?,
);
}
/// Serialize to JSON
Map<String, dynamic> toJson() {
return {
'target_language': targetLanguage,
'language_of_instructions': languageOfInstructions,
'cefr_level': cefrLevel.string,
'title': title,
'description': description,
'id': uuid,
'topics': topics.map((e) => e.toJson()).toList(),
'image_url': imageUrl,
};
}
}

@ -1,97 +0,0 @@
import 'package:collection/collection.dart';
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/courses/test_courses_json.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
class CourseFilter {
final LanguageModel? targetLanguage;
final LanguageModel? languageOfInstructions;
final LanguageLevelTypeEnum? cefrLevel;
CourseFilter({
this.targetLanguage,
this.languageOfInstructions,
this.cefrLevel,
});
}
class CourseRepo {
static final GetStorage _courseStorage = GetStorage("course_storage");
static CoursePlanModel? _getCached(String id) {
final json = _courseStorage.read(id);
if (json != null) {
try {
return CoursePlanModel.fromJson(json);
} catch (e) {
_courseStorage.remove(id);
}
}
return null;
}
static List<CoursePlanModel> _getAllCached() {
final keys = _courseStorage.getKeys();
return keys
.map((key) => _getCached(key))
.whereType<CoursePlanModel>()
.toList();
}
static Future<void> set(CoursePlanModel coursePlan) async {
await _courseStorage.write(coursePlan.uuid, coursePlan.toJson());
}
static Future<CoursePlanModel?> get(String id) async {
final cached = _getCached(id);
if (cached != null) {
return cached;
}
final resp = await search();
return resp.firstWhereOrNull((course) => course.uuid == id);
}
static Future<List<CoursePlanModel>> search({CourseFilter? filter}) async {
final cached = _getAllCached();
if (cached.isNotEmpty) {
return cached.filtered(filter);
}
final resp = (courseJson["courses"] as List<dynamic>)
.map((json) => CoursePlanModel.fromJson(json))
.whereType<CoursePlanModel>()
.toList();
for (final plan in resp) {
set(plan);
}
return resp.filtered(filter);
}
}
extension on List<CoursePlanModel> {
List<CoursePlanModel> filtered(CourseFilter? filter) {
return where((course) {
final matchesTargetLanguage = filter?.targetLanguage == null ||
course.targetLanguage.split("-").first ==
filter?.targetLanguage?.langCodeShort;
final matchesLanguageOfInstructions =
filter?.languageOfInstructions == null ||
course.languageOfInstructions.split("-").first ==
filter?.languageOfInstructions?.langCodeShort;
final matchesCefrLevel =
filter?.cefrLevel == null || course.cefrLevel == filter?.cefrLevel;
return matchesTargetLanguage &&
matchesLanguageOfInstructions &&
matchesCefrLevel;
}).toList();
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,32 @@
class JoinField {
final List<String>? docs;
final bool? hasNextPage;
final int? totalDocs;
const JoinField({
this.docs,
this.hasNextPage,
this.totalDocs,
});
factory JoinField.fromJson(
Map<String, dynamic> json,
) {
final raw = json['docs'];
final list = (raw is List) ? raw.map((e) => e as String).toList() : null;
return JoinField(
docs: list,
hasNextPage: json['hasNextPage'] as bool?,
totalDocs: json['totalDocs'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'docs': docs,
'hasNextPage': hasNextPage,
'totalDocs': totalDocs,
};
}
}

@ -0,0 +1,67 @@
import 'package:fluffychat/pangea/payload_client/join_field.dart';
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents a course plan from the CMS API
class CmsCoursePlan {
final String id;
final String title;
final String description;
final String cefrLevel;
final String l1; // Language of instruction
final String l2; // Target language
final JoinField? coursePlanMedia;
final JoinField? coursePlanModules;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String updatedAt;
final String createdAt;
CmsCoursePlan({
required this.id,
required this.title,
required this.description,
required this.cefrLevel,
required this.l1,
required this.l2,
this.coursePlanMedia,
this.coursePlanModules,
this.createdBy,
this.updatedBy,
required this.updatedAt,
required this.createdAt,
});
factory CmsCoursePlan.fromJson(Map<String, dynamic> json) {
return CmsCoursePlan(
id: json['id'],
title: json['title'],
description: json['description'],
cefrLevel: json['cefrLevel'],
l1: json['l1'],
l2: json['l2'],
coursePlanMedia: JoinField.fromJson(json['coursePlanMedia']),
coursePlanModules: JoinField.fromJson(json['coursePlanModules']),
createdBy: PolymorphicRelationship.fromJson(json['createdBy']),
updatedBy: PolymorphicRelationship.fromJson(json['updatedBy']),
updatedAt: json['updatedAt'],
createdAt: json['createdAt'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'cefrLevel': cefrLevel,
'l1': l1,
'l2': l2,
'coursePlanMedia': coursePlanMedia?.toJson(),
'coursePlanModules': coursePlanModules?.toJson(),
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'updatedAt': updatedAt,
'createdAt': createdAt,
};
}
}

@ -0,0 +1,164 @@
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/payload_client/join_field.dart';
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents a course plan activity role
class CmsCoursePlanActivityRole {
final String id;
final String name;
final String goal;
final String? avatarUrl;
CmsCoursePlanActivityRole({
required this.id,
required this.name,
required this.goal,
this.avatarUrl,
});
factory CmsCoursePlanActivityRole.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanActivityRole(
id: json['id'] as String,
name: json['name'] as String,
goal: json['goal'] as String,
avatarUrl: json['avatarUrl'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'goal': goal,
'avatarUrl': avatarUrl,
};
}
}
/// Represents vocabulary in a course plan activity
class CmsCoursePlanVocab {
final String lemma;
final String pos;
final String? id;
CmsCoursePlanVocab({
required this.lemma,
required this.pos,
this.id,
});
factory CmsCoursePlanVocab.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanVocab(
lemma: json['lemma'] as String,
pos: json['pos'] as String,
id: json['id'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'pos': pos,
'id': id,
};
}
}
/// Represents a course plan activity from the CMS API
class CmsCoursePlanActivity {
final String id;
final String title;
final String description;
final String learningObjective;
final String instructions;
final String l1; // Language of instruction
final String l2; // Target language
final LanguageLevelTypeEnum cefrLevel;
final List<CmsCoursePlanActivityRole> roles;
final List<CmsCoursePlanVocab> vocabs;
final JoinField? coursePlanActivityMedia;
final List<String> coursePlanModules;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String updatedAt;
final String createdAt;
CmsCoursePlanActivity({
required this.id,
required this.title,
required this.description,
required this.learningObjective,
required this.instructions,
required this.l1,
required this.l2,
required this.cefrLevel,
required this.roles,
required this.vocabs,
required this.coursePlanActivityMedia,
required this.coursePlanModules,
this.createdBy,
this.updatedBy,
required this.updatedAt,
required this.createdAt,
});
factory CmsCoursePlanActivity.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanActivity(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
learningObjective: json['learningObjective'] as String,
instructions: json['instructions'] as String,
l1: json['l1'] as String,
l2: json['l2'] as String,
cefrLevel: LanguageLevelTypeEnumExtension.fromString(
json['cefrLevel'] as String,
),
roles: (json['roles'] as List<dynamic>)
.map(
(role) => CmsCoursePlanActivityRole.fromJson(
role as Map<String, dynamic>,
),
)
.toList(),
vocabs: (json['vocabs'] as List<dynamic>)
.map(
(vocab) =>
CmsCoursePlanVocab.fromJson(vocab as Map<String, dynamic>),
)
.toList(),
coursePlanActivityMedia:
JoinField.fromJson(json['coursePlanActivityMedia']),
coursePlanModules: List<String>.from(json['coursePlanModules']),
createdBy: json['createdBy'] != null
? PolymorphicRelationship.fromJson(json['createdBy'])
: null,
updatedBy: json['updatedBy'] != null
? PolymorphicRelationship.fromJson(json['updatedBy'])
: null,
updatedAt: json['updatedAt'] as String,
createdAt: json['createdAt'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'learningObjective': learningObjective,
'instructions': instructions,
'l1': l1,
'l2': l2,
'cefrLevel': cefrLevel.string,
'roles': roles.map((role) => role.toJson()).toList(),
'vocabs': vocabs.map((vocab) => vocab.toJson()).toList(),
'coursePlanActivityMedia': coursePlanActivityMedia?.toJson(),
'coursePlanModules': coursePlanModules,
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'updatedAt': updatedAt,
'createdAt': createdAt,
};
}
}

@ -0,0 +1,90 @@
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents course plan activity media from the CMS API
class CmsCoursePlanActivityMedia {
final String id;
final String? alt;
final List<String> coursePlanActivities;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String? prefix;
final String updatedAt;
final String createdAt;
final String? url;
final String? thumbnailURL;
final String? filename;
final String? mimeType;
final int? filesize;
final int? width;
final int? height;
final double? focalX;
final double? focalY;
CmsCoursePlanActivityMedia({
required this.id,
this.alt,
required this.coursePlanActivities,
this.createdBy,
this.updatedBy,
this.prefix,
required this.updatedAt,
required this.createdAt,
this.url,
this.thumbnailURL,
this.filename,
this.mimeType,
this.filesize,
this.width,
this.height,
this.focalX,
this.focalY,
});
factory CmsCoursePlanActivityMedia.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanActivityMedia(
id: json['id'] as String,
alt: json['alt'] as String?,
coursePlanActivities: List<String>.from(json['coursePlanActivities']),
createdBy: json['createdBy'] != null
? PolymorphicRelationship.fromJson(json['createdBy'])
: null,
updatedBy: json['updatedBy'] != null
? PolymorphicRelationship.fromJson(json['updatedBy'])
: null,
prefix: json['prefix'] as String?,
updatedAt: json['updatedAt'] as String,
createdAt: json['createdAt'] as String,
url: json['url'] as String?,
thumbnailURL: json['thumbnailURL'] as String?,
filename: json['filename'] as String?,
mimeType: json['mimeType'] as String?,
filesize: json['filesize'] as int?,
width: json['width'] as int?,
height: json['height'] as int?,
focalX: (json['focalX'] as num?)?.toDouble(),
focalY: (json['focalY'] as num?)?.toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'alt': alt,
'coursePlanActivities': coursePlanActivities,
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'prefix': prefix,
'updatedAt': updatedAt,
'createdAt': createdAt,
'url': url,
'thumbnailURL': thumbnailURL,
'filename': filename,
'mimeType': mimeType,
'filesize': filesize,
'width': width,
'height': height,
'focalX': focalX,
'focalY': focalY,
};
}
}

@ -0,0 +1,90 @@
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents course plan media from the CMS API
class CmsCoursePlanMedia {
final String id;
final String? alt;
final List<String> coursePlans;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String? prefix;
final String updatedAt;
final String createdAt;
final String? url;
final String? thumbnailURL;
final String? filename;
final String? mimeType;
final int? filesize;
final int? width;
final int? height;
final double? focalX;
final double? focalY;
CmsCoursePlanMedia({
required this.id,
this.alt,
required this.coursePlans,
this.createdBy,
this.updatedBy,
this.prefix,
required this.updatedAt,
required this.createdAt,
this.url,
this.thumbnailURL,
this.filename,
this.mimeType,
this.filesize,
this.width,
this.height,
this.focalX,
this.focalY,
});
factory CmsCoursePlanMedia.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanMedia(
id: json['id'],
alt: json['alt'],
coursePlans: List<String>.from(json['coursePlans'] as List),
createdBy: json['createdBy'] != null
? PolymorphicRelationship.fromJson(json['createdBy'])
: null,
updatedBy: json['updatedBy'] != null
? PolymorphicRelationship.fromJson(json['updatedBy'])
: null,
prefix: json['prefix'],
updatedAt: json['updatedAt'],
createdAt: json['createdAt'],
url: json['url'],
thumbnailURL: json['thumbnailURL'],
filename: json['filename'],
mimeType: json['mimeType'],
filesize: json['filesize'],
width: json['width'],
height: json['height'],
focalX: json['focalX']?.toDouble(),
focalY: json['focalY']?.toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'alt': alt,
'coursePlans': coursePlans,
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'prefix': prefix,
'updatedAt': updatedAt,
'createdAt': createdAt,
'url': url,
'thumbnailURL': thumbnailURL,
'filename': filename,
'mimeType': mimeType,
'filesize': filesize,
'width': width,
'height': height,
'focalX': focalX,
'focalY': focalY,
};
}
}

@ -0,0 +1,67 @@
import 'package:fluffychat/pangea/payload_client/join_field.dart';
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents a course plan module from the CMS API
class CmsCoursePlanModule {
final String id;
final String title;
final String description;
final JoinField? coursePlanActivities;
final JoinField? coursePlanModuleLocations;
final List<String> coursePlans;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String updatedAt;
final String createdAt;
CmsCoursePlanModule({
required this.id,
required this.title,
required this.description,
required this.coursePlanActivities,
required this.coursePlanModuleLocations,
required this.coursePlans,
this.createdBy,
this.updatedBy,
required this.updatedAt,
required this.createdAt,
});
factory CmsCoursePlanModule.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanModule(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
coursePlanActivities: JoinField.fromJson(
json['coursePlanActivities'],
),
coursePlanModuleLocations: JoinField.fromJson(
json['coursePlanModuleLocations'],
),
coursePlans: List<String>.from(json['coursePlans']),
createdBy: json['createdBy'] != null
? PolymorphicRelationship.fromJson(json['createdBy'])
: null,
updatedBy: json['updatedBy'] != null
? PolymorphicRelationship.fromJson(json['updatedBy'])
: null,
updatedAt: json['updatedAt'] as String,
createdAt: json['createdAt'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'coursePlanActivities': coursePlanActivities?.toJson(),
'coursePlanModuleLocations': coursePlanModuleLocations?.toJson(),
'coursePlans': coursePlans,
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'updatedAt': updatedAt,
'createdAt': createdAt,
};
}
}

@ -0,0 +1,57 @@
import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart';
/// Represents a course plan module location from the CMS API
class CmsCoursePlanModuleLocation {
final String id;
final String name;
// [longitude, latitude] - minItems: 2, maxItems: 2
final List<double>? coordinates;
final List<String> coursePlanModules;
final PolymorphicRelationship? createdBy;
final PolymorphicRelationship? updatedBy;
final String updatedAt;
final String createdAt;
CmsCoursePlanModuleLocation({
required this.id,
required this.name,
this.coordinates,
required this.coursePlanModules,
this.createdBy,
this.updatedBy,
required this.updatedAt,
required this.createdAt,
});
factory CmsCoursePlanModuleLocation.fromJson(Map<String, dynamic> json) {
return CmsCoursePlanModuleLocation(
id: json['id'] as String,
name: json['name'] as String,
coordinates: (json['coordinates'] as List<dynamic>?)
?.map((coord) => (coord as num).toDouble())
.toList(),
coursePlanModules: List<String>.from(json['coursePlanModules']),
createdBy: json['createdBy'] != null
? PolymorphicRelationship.fromJson(json['createdBy'])
: null,
updatedBy: json['updatedBy'] != null
? PolymorphicRelationship.fromJson(json['updatedBy'])
: null,
updatedAt: json['updatedAt'] as String,
createdAt: json['createdAt'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'coordinates': coordinates,
'coursePlanModules': coursePlanModules,
'createdBy': createdBy?.toJson(),
'updatedBy': updatedBy?.toJson(),
'updatedAt': updatedAt,
'createdAt': createdAt,
};
}
}

@ -0,0 +1,181 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Response model for paginated results from PayloadCMS
class PayloadPaginatedResponse<T> {
final List<T> docs;
final int totalDocs;
final int limit;
final int page;
final int totalPages;
final bool hasNextPage;
final bool hasPrevPage;
final int? nextPage;
final int? prevPage;
PayloadPaginatedResponse({
required this.docs,
required this.totalDocs,
required this.limit,
required this.page,
required this.totalPages,
required this.hasNextPage,
required this.hasPrevPage,
this.nextPage,
this.prevPage,
});
factory PayloadPaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
return PayloadPaginatedResponse<T>(
docs: (json['docs'] as List<dynamic>)
.map((e) => fromJsonT(e as Map<String, dynamic>))
.toList(),
totalDocs: json['totalDocs'] as int,
limit: json['limit'] as int,
page: json['page'] as int,
totalPages: json['totalPages'] as int,
hasNextPage: json['hasNextPage'] as bool,
hasPrevPage: json['hasPrevPage'] as bool,
nextPage: json['nextPage'] as int?,
prevPage: json['prevPage'] as int?,
);
}
}
/// Generic PayloadCMS client for CRUD operations
class PayloadClient {
final String baseUrl;
final String accessToken;
final String basePath = "/cms/api";
PayloadClient({
required this.baseUrl,
required this.accessToken,
});
Map<String, String> get _headers {
final headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
headers['Authorization'] = 'Bearer $accessToken';
return headers;
}
/// Generic GET request
Future<http.Response> get(String endpoint) async {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.get(url, headers: _headers);
return response;
}
/// Generic POST request
Future<http.Response> post(String endpoint, Map<String, dynamic> body) async {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.post(
url,
headers: _headers,
body: jsonEncode(body),
);
return response;
}
/// Generic PATCH request
Future<http.Response> patch(
String endpoint,
Map<String, dynamic> body,
) async {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.patch(
url,
headers: _headers,
body: jsonEncode(body),
);
return response;
}
/// Generic DELETE request
Future<http.Response> delete(String endpoint) async {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.delete(url, headers: _headers);
return response;
}
/// Find documents with pagination
Future<PayloadPaginatedResponse<T>> find<T>(
String collection,
T Function(Map<String, dynamic>) fromJson, {
int? page,
int? limit,
Map<String, dynamic>? where,
String? sort,
}) async {
final queryParams = <String, String>{};
if (page != null) queryParams['page'] = page.toString();
if (limit != null) queryParams['limit'] = limit.toString();
if (where != null) queryParams['where'] = jsonEncode(where);
if (sort != null) queryParams['sort'] = sort;
final endpoint =
'$basePath/$collection${queryParams.isNotEmpty ? '?${Uri(queryParameters: queryParams).query}' : ''}';
final response = await get(endpoint);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return PayloadPaginatedResponse.fromJson(json, fromJson);
}
/// Find a single document by ID
Future<T> findById<T>(
String collection,
String id,
T Function(Map<String, dynamic>) fromJson,
) async {
final endpoint = '$basePath/$collection/$id';
final response = await get(endpoint);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
/// Create a new document
Future<T> createDocument<T>(
String collection,
Map<String, dynamic> data,
T Function(Map<String, dynamic>) fromJson,
) async {
final endpoint = '$basePath/$collection';
final response = await post(endpoint, data);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
/// Update an existing document
Future<T> updateDocument<T>(
String collection,
String id,
Map<String, dynamic> data,
T Function(Map<String, dynamic>) fromJson,
) async {
final endpoint = '$basePath/$collection/$id';
final response = await patch(endpoint, data);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
/// Delete a document
Future<T> deleteDocument<T>(
String collection,
String id,
T Function(Map<String, dynamic>) fromJson,
) async {
final endpoint = '$basePath/$collection/$id';
final response = await delete(endpoint);
final json = jsonDecode(response.body) as Map<String, dynamic>;
return fromJson(json);
}
}

@ -0,0 +1,23 @@
class PolymorphicRelationship {
final String relationTo;
final String value;
PolymorphicRelationship({
required this.relationTo,
required this.value,
});
factory PolymorphicRelationship.fromJson(Map<String, dynamic> json) {
return PolymorphicRelationship(
relationTo: json['relationTo'] as String,
value: json['value'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'relationTo': relationTo,
'value': value,
};
}
}
Loading…
Cancel
Save