feat: courses repo (#3777)
parent
4e3f82331c
commit
a62d9f8643
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue