diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts
new file mode 100644
index 000000000..513b7aee1
--- /dev/null
+++ b/src/core/features/courses/components/components.module.ts
@@ -0,0 +1,44 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+
+import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
+
+@NgModule({
+ declarations: [
+ CoreCoursesCourseListItemComponent,
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule,
+ ],
+ providers: [
+ ],
+ exports: [
+ CoreCoursesCourseListItemComponent,
+ ],
+})
+export class CoreCoursesComponentsModule {}
diff --git a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html
new file mode 100644
index 000000000..1495e6d9f
--- /dev/null
+++ b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts
new file mode 100644
index 000000000..3faa41bce
--- /dev/null
+++ b/src/core/features/courses/components/course-list-item/course-list-item.ts
@@ -0,0 +1,105 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit } from '@angular/core';
+import { NavController } from '@ionic/angular';
+import { CoreCourseHelper } from '@features/course/services/course.helper';
+import { CoreCourses, CoreCourseSearchedData } from '@features/courses/services/courses';
+
+/**
+ * This directive is meant to display an item for a list of courses.
+ *
+ * Example usage:
+ *
+ *
+ */
+@Component({
+ selector: 'core-courses-course-list-item',
+ templateUrl: 'core-courses-course-list-item.html',
+})
+export class CoreCoursesCourseListItemComponent implements OnInit {
+
+ @Input() course!: CoreCourseSearchedData; // The course to render.
+
+ icons: CoreCoursesEnrolmentIcons[] = [];
+ isEnrolled = false;
+
+ constructor(
+ protected navCtrl: NavController,
+ ) {
+ }
+
+ /**
+ * Component being initialized.
+ */
+ async ngOnInit(): Promise {
+ // Check if the user is enrolled in the course.
+ try {
+ await CoreCourses.instance.getUserCourse(this.course.id);
+
+ this.isEnrolled = true;
+ } catch {
+ this.isEnrolled = false;
+ this.icons = [];
+
+ this.course.enrollmentmethods.forEach((instance) => {
+ if (instance === 'self') {
+ this.icons.push({
+ label: 'core.courses.selfenrolment',
+ icon: 'fas-key',
+ });
+ } else if (instance === 'guest') {
+ this.icons.push({
+ label: 'core.courses.allowguests',
+ icon: 'fas-unlock',
+ });
+ } else if (instance === 'paypal') {
+ this.icons.push({
+ label: 'core.courses.paypalaccepted',
+ icon: 'fab-paypal',
+ });
+ }
+ });
+
+ if (this.icons.length == 0) {
+ this.icons.push({
+ label: 'core.courses.notenrollable',
+ icon: 'fas-lock',
+ });
+ }
+ }
+ }
+
+ /**
+ * Open a course.
+ *
+ * @param course The course to open.
+ */
+ openCourse(): void {
+ if (this.isEnrolled) {
+ CoreCourseHelper.instance.openCourse(this.course);
+ } else {
+ this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
+ }
+ }
+
+}
+
+/**
+ * Enrolment icons to show on the list with a label.
+ */
+export type CoreCoursesEnrolmentIcons = {
+ label: string;
+ icon: string;
+};
diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts
index 570eb5569..b1a4e1cac 100644
--- a/src/core/features/courses/courses.module.ts
+++ b/src/core/features/courses/courses.module.ts
@@ -13,12 +13,12 @@
// limitations under the License.
import { NgModule } from '@angular/core';
-import { Routes } from '@angular/router';
+import { RouterModule, Routes } from '@angular/router';
import { CoreHomeRoutingModule } from '../mainmenu/pages/home/home-routing.module';
import { CoreHomeDelegate } from '../mainmenu/services/home.delegate';
import { CoreDashboardHomeHandler } from './services/handlers/dashboard.home';
-const routes: Routes = [
+const homeRoutes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
@@ -26,9 +26,50 @@ const routes: Routes = [
},
];
+const routes: Routes = [
+ {
+ path: 'courses',
+ children: [
+ {
+ path: '',
+ redirectTo: 'all',
+ pathMatch: 'full',
+ },
+ {
+ path: 'categories',
+ redirectTo: 'categories/root', // Fake "id".
+ pathMatch: 'full',
+ },
+ {
+ path: 'categories/:id',
+ loadChildren: () =>
+ import('@features/courses/pages/categories/categories.page.module').then(m => m.CoreCoursesCategoriesPageModule),
+ },
+ {
+ path: 'all',
+ loadChildren: () =>
+ import('@features/courses/pages/available-courses/available-courses.page.module')
+ .then(m => m.CoreCoursesAvailableCoursesPageModule),
+ },
+ {
+ path: 'search',
+ loadChildren: () =>
+ import('@features/courses/pages/search/search.page.module')
+ .then(m => m.CoreCoursesSearchPageModule),
+ },
+ ],
+ },
+];
+
@NgModule({
- imports: [CoreHomeRoutingModule.forChild(routes)],
- exports: [CoreHomeRoutingModule],
+ imports: [
+ CoreHomeRoutingModule.forChild(homeRoutes),
+ RouterModule.forChild(routes),
+ ],
+ exports: [
+ CoreHomeRoutingModule,
+ RouterModule,
+ ],
providers: [
CoreDashboardHomeHandler,
],
diff --git a/src/core/features/courses/pages/available-courses/available-courses.html b/src/core/features/courses/pages/available-courses/available-courses.html
new file mode 100644
index 000000000..eafff0704
--- /dev/null
+++ b/src/core/features/courses/pages/available-courses/available-courses.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{ 'core.courses.availablecourses' | translate }}
+
+
+
+
+
+
+
+ 0">
+
+
+
+
+
diff --git a/src/core/features/courses/pages/available-courses/available-courses.page.module.ts b/src/core/features/courses/pages/available-courses/available-courses.page.module.ts
new file mode 100644
index 000000000..ef135f506
--- /dev/null
+++ b/src/core/features/courses/pages/available-courses/available-courses.page.module.ts
@@ -0,0 +1,50 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule, Routes } from '@angular/router';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreCoursesComponentsModule } from '../../components/components.module';
+
+import { CoreCoursesAvailableCoursesPage } from './available-courses.page';
+
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CoreCoursesAvailableCoursesPage,
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CoreCoursesComponentsModule,
+ ],
+ declarations: [
+ CoreCoursesAvailableCoursesPage,
+ ],
+ exports: [RouterModule],
+})
+export class CoreCoursesAvailableCoursesPageModule { }
diff --git a/src/core/features/courses/pages/available-courses/available-courses.page.ts b/src/core/features/courses/pages/available-courses/available-courses.page.ts
new file mode 100644
index 000000000..d979dbd41
--- /dev/null
+++ b/src/core/features/courses/pages/available-courses/available-courses.page.ts
@@ -0,0 +1,78 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnInit } from '@angular/core';
+import { IonRefresher } from '@ionic/angular';
+
+import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
+
+/**
+ * Page that displays available courses in current site.
+ */
+@Component({
+ selector: 'page-core-courses-available-courses',
+ templateUrl: 'available-courses.html',
+})
+export class CoreCoursesAvailableCoursesPage implements OnInit {
+
+ courses: CoreCourseSearchedData[] = [];
+ coursesLoaded = false;
+
+ /**
+ * View loaded.
+ */
+ ngOnInit(): void {
+ this.loadCourses().finally(() => {
+ this.coursesLoaded = true;
+ });
+ }
+
+ /**
+ * Load the courses.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async loadCourses(): Promise {
+ const frontpageCourseId = CoreSites.instance.getCurrentSite()!.getSiteHomeId();
+
+ try {
+ const courses = await CoreCourses.instance.getCoursesByField();
+
+ this.courses = courses.filter((course) => course.id != frontpageCourseId);
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
+ }
+ }
+
+ /**
+ * Refresh the courses.
+ *
+ * @param refresher Refresher.
+ */
+ refreshCourses(refresher: CustomEvent): void {
+ const promises: Promise[] = [];
+
+ promises.push(CoreCourses.instance.invalidateUserCourses());
+ promises.push(CoreCourses.instance.invalidateCoursesByField());
+
+ Promise.all(promises).finally(() => {
+ this.loadCourses().finally(() => {
+ refresher?.detail.complete();
+ });
+ });
+ }
+
+}
diff --git a/src/core/features/courses/pages/categories/categories.html b/src/core/features/courses/pages/categories/categories.html
new file mode 100644
index 000000000..7af0a9e5b
--- /dev/null
+++ b/src/core/features/courses/pages/categories/categories.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+