diff --git a/classes/output/core/course_renderer.php b/classes/output/core/course_renderer.php
new file mode 100644
index 00000000000..6ad4903ccbf
--- /dev/null
+++ b/classes/output/core/course_renderer.php
@@ -0,0 +1,251 @@
+.
+
+namespace theme_boost_union\output\core;
+
+use html_writer;
+use coursecat_helper;
+use stdClass;
+use core_course_list_element;
+use theme_boost_union\util\course;
+use moodle_url;
+
+/**
+ * Renderers to align Boost Union's course elements to what is expect
+ *
+ * @package theme_boost_union
+ * @copyright 2024 Daniel Neis Araujo {@link https://www.adapta.online}
+ * @copyright 2022 Willian Mano {@link https://conecti.me}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_renderer extends \core_course_renderer {
+
+ /**
+ * Renders the list of courses
+ *
+ * This is internal function, please use core_course_renderer::courses_list() or another public
+ * method from outside of the class
+ *
+ * If list of courses is specified in $courses; the argument $chelper is only used
+ * to retrieve display options and attributes, only methods get_show_courses(),
+ * get_courses_display_option() and get_and_erase_attributes() are called.
+ *
+ * @param coursecat_helper $chelper various display options
+ * @param array $courses the list of courses to display
+ * @param int|null $totalcount total number of courses (affects display mode if it is AUTO or pagination if applicable),
+ * defaulted to count($courses)
+ * @return string
+ */
+ protected function coursecat_courses(coursecat_helper $chelper, $courses, $totalcount = null) {
+ if (!get_config('theme_boost_union', 'enablecards')) {
+ return parent::coursecat_courses($chelper, $courses, $totalcount);
+ }
+ global $CFG;
+ if ($totalcount === null) {
+ $totalcount = count($courses);
+ }
+ if (!$totalcount) {
+ // Courses count is cached during courses retrieval.
+ return '';
+ }
+
+ if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_AUTO) {
+ // In 'auto' course display mode we analyse if number of courses is more or less than $CFG->courseswithsummarieslimit.
+ if ($totalcount <= $CFG->courseswithsummarieslimit) {
+ $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED);
+ } else {
+ $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_COLLAPSED);
+ }
+ }
+
+ // Prepare content of paging bar if it is needed.
+ $paginationurl = $chelper->get_courses_display_option('paginationurl');
+ $paginationallowall = $chelper->get_courses_display_option('paginationallowall');
+ if ($totalcount > count($courses)) {
+ // There are more results that can fit on one page.
+ if ($paginationurl) {
+ // The option paginationurl was specified, display pagingbar.
+ $perpage = $chelper->get_courses_display_option('limit', $CFG->coursesperpage);
+ $page = $chelper->get_courses_display_option('offset') / $perpage;
+ $pagingbar = $this->paging_bar($totalcount, $page, $perpage,
+ $paginationurl->out(false, ['perpage' => $perpage]));
+ if ($paginationallowall) {
+ $pagingbar .= html_writer::tag('div', html_writer::link($paginationurl->out(false, ['perpage' => 'all']),
+ get_string('showall', '', $totalcount)), ['class' => 'paging paging-showall']);
+ }
+ } else if ($viewmoreurl = $chelper->get_courses_display_option('viewmoreurl')) {
+ // The option for 'View more' link was specified, display more link.
+ $viewmoretext = $chelper->get_courses_display_option('viewmoretext', new \lang_string('viewmore'));
+ $morelink = html_writer::tag(
+ 'div',
+ html_writer::link($viewmoreurl, $viewmoretext, ['class' => 'btn btn-secondary']),
+ ['class' => 'paging paging-morelink']
+ );
+ }
+ } else if (($totalcount > $CFG->coursesperpage) && $paginationurl && $paginationallowall) {
+ // There are more than one page of results and we are in 'view all' mode, suggest to go back to paginated view mode.
+ $pagingbar = html_writer::tag('div',
+ html_writer::link($paginationurl->out(false, ['perpage' => $CFG->coursesperpage]),
+ get_string('showperpage', '', $CFG->coursesperpage)), ['class' => 'paging paging-showperpage']);
+ }
+
+ // Display list of courses.
+ $attributes = $chelper->get_and_erase_attributes('courses');
+ $content = html_writer::start_tag('div', $attributes);
+
+ if (!empty($pagingbar)) {
+ $content .= $pagingbar;
+ }
+
+ $content .= html_writer::start_tag('div', ['class' => 'card-deck dashboard-card-deck']);
+ foreach ($courses as $course) {
+ $content .= $this->coursecat_coursebox($chelper, $course);
+ }
+ $content .= html_writer::end_tag('div');
+
+ if (!empty($pagingbar)) {
+ $content .= $pagingbar;
+ }
+
+ if (!empty($morelink)) {
+ $content .= $morelink;
+ }
+
+ $content .= html_writer::end_tag('div'); // End courses.
+
+ return $content;
+ }
+
+ /**
+ * Displays one course in the list of courses.
+ *
+ * This is an internal function, to display an information about just one course
+ * please use core_course_renderer::course_info_box()
+ *
+ * @param coursecat_helper $chelper various display options
+ * @param core_course_list_element|stdClass $course
+ * @param string $additionalclasses additional classes to add to the main
tag (usually
+ * depend on the course position in list - first/last/even/odd)
+ * @return string
+ *
+ * @throws \coding_exception
+ * @throws \dml_exception
+ * @throws \moodle_exception
+ */
+ protected function coursecat_coursebox(coursecat_helper $chelper, $course, $additionalclasses = '') {
+ if (!get_config('theme_boost_union', 'enablecards')) {
+ return parent::coursecat_coursebox($chelper, $course, $additionalclasses);
+ }
+ if (!isset($this->strings->summary)) {
+ $this->strings->summary = get_string('summary');
+ }
+
+ if ($chelper->get_show_courses() <= self::COURSECAT_SHOW_COURSES_COUNT) {
+ return '';
+ }
+
+ if ($course instanceof stdClass) {
+ $course = new core_course_list_element($course);
+ }
+
+ return $this->coursecat_coursebox_content($chelper, $course);
+ }
+
+ /**
+ * Returns HTML to display course content (summary, course contacts and optionally category name)
+ *
+ * This method is called from coursecat_coursebox() and may be re-used in AJAX
+ *
+ * @param coursecat_helper $chelper various display options
+ * @param stdClass|core_course_list_element $course
+ *
+ * @return string
+ *
+ * @throws \coding_exception
+ * @throws \dml_exception
+ * @throws \moodle_exception
+ */
+ protected function coursecat_coursebox_content(coursecat_helper $chelper, $course) {
+ if (!get_config('theme_boost_union', 'enablecards')) {
+ return parent::coursecat_coursebox_content($chelper, $course);
+ }
+ if ($course instanceof stdClass) {
+ $course = new core_course_list_element($course);
+ }
+
+ $courseutil = new course($course);
+
+ $coursecontacts = $courseutil->get_course_contacts();
+
+ $courseenrolmenticons = $courseutil->get_enrolment_icons();
+ $courseenrolmenticons = !empty($courseenrolmenticons) ? $this->render_enrolment_icons($courseenrolmenticons) : false;
+
+ $courseprogress = $courseutil->get_progress();
+ $hasprogress = $courseprogress != null;
+
+ $data = [
+ 'id' => $course->id,
+ 'fullname' => $chelper->get_course_formatted_name($course),
+ 'visible' => $course->visible,
+ 'image' => $courseutil->get_summary_image(),
+ 'summary' => $courseutil->get_summary($chelper),
+ 'category' => $courseutil->get_category(),
+ 'customfields' => $courseutil->get_custom_fields(),
+ 'hasprogress' => $hasprogress,
+ 'progress' => (int) $courseprogress,
+ 'hasenrolmenticons' => $courseenrolmenticons != false,
+ 'enrolmenticons' => $courseenrolmenticons,
+ 'hascontacts' => !empty($coursecontacts),
+ 'contacts' => $coursecontacts,
+ 'courseurl' => $this->get_course_url($course->id),
+ ];
+
+ return $this->render_from_template('theme_boost_union/coursecard', $data);
+ }
+
+ /**
+ * Returns enrolment icons
+ *
+ * @param array $icons
+ *
+ * @return array
+ */
+ protected function render_enrolment_icons(array $icons): array {
+ $data = [];
+
+ foreach ($icons as $icon) {
+ $data[] = $this->render($icon);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Returns the course URL based on some criterias.
+ *
+ * @param int $courseid
+ *
+ * @return moodle_url
+ * @throws \moodle_exception
+ */
+ private function get_course_url($courseid) {
+ if (class_exists('\local_course\output\index')) {
+ return new moodle_url('/local/course/index.php', ['id' => $courseid]);
+ }
+
+ return new moodle_url('/course/view.php', ['id' => $courseid]);
+ }
+}
diff --git a/classes/util/course.php b/classes/util/course.php
new file mode 100644
index 00000000000..7d4814192f7
--- /dev/null
+++ b/classes/util/course.php
@@ -0,0 +1,165 @@
+.
+
+namespace theme_boost_union\util;
+
+use moodle_url;
+use core_course_list_element;
+use coursecat_helper;
+use core_course_category;
+
+/**
+ * Course class utility class
+ *
+ * @package theme_boost_union
+ * @copyright 2022 Willian Mano {@link https://conecti.me}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course {
+ /**
+ * @var \stdClass $course The course object.
+ */
+ protected $course;
+
+ /**
+ * Class constructor
+ *
+ * @param core_course_list_element $course
+ *
+ */
+ public function __construct($course) {
+ $this->course = $course;
+ }
+
+ /**
+ * Returns the first course's summary image url
+ *
+ * @return string
+ */
+ public function get_summary_image() {
+ global $CFG, $OUTPUT;
+
+ foreach ($this->course->get_course_overviewfiles() as $file) {
+ if ($file->is_valid_image()) {
+ $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php",
+ '/' . $file->get_contextid() . '/' . $file->get_component() . '/' .
+ $file->get_filearea() . $file->get_filepath() . $file->get_filename(), !$file->is_valid_image());
+
+ return $url->out();
+ }
+ }
+
+ return $OUTPUT->get_generated_image_for_id($this->course->id);
+ }
+
+ /**
+ * Returns HTML to display course contacts.
+ *
+ * @return array
+ */
+ public function get_course_contacts() {
+
+ $contacts = [];
+ if (get_config('theme_boost_union', 'enableteacherspic') && $this->course->has_course_contacts()) {
+ $instructors = $this->course->get_course_contacts();
+
+ foreach ($instructors as $instructor) {
+ $user = $instructor['user'];
+ $userutil = new user($user->id);
+
+ $contacts[] = [
+ 'id' => $user->id,
+ 'fullname' => fullname($user),
+ 'userpicture' => $userutil->get_user_picture(),
+ 'role' => $instructor['role']->displayname,
+ ];
+ }
+ }
+
+ return $contacts;
+ }
+
+ /**
+ * Returns HTML to display course category name.
+ *
+ * @return string
+ *
+ * @throws \moodle_exception
+ */
+ public function get_category(): string {
+ $cat = core_course_category::get($this->course->category, IGNORE_MISSING);
+
+ if (!$cat) {
+ return '';
+ }
+
+ return $cat->get_formatted_name();
+ }
+
+ /**
+ * Returns course summary.
+ *
+ * @param coursecat_helper $chelper
+ */
+ public function get_summary(coursecat_helper $chelper): string {
+ if (get_config('theme_boost_union', 'enablecardssummary') && $this->course->has_summary()) {
+ return $chelper->get_course_formatted_summary($this->course,
+ ['overflowdiv' => true, 'noclean' => true, 'para' => false]
+ );
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns course custom fields.
+ *
+ * @return string
+ */
+ public function get_custom_fields(): string {
+ if (get_config('theme_boost_union', 'enablecardscfields') && $this->course->has_custom_fields()) {
+ $handler = \core_course\customfield\course_handler::create();
+
+ return $handler->display_custom_fields_data($this->course->get_custom_fields());
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns HTML to display course enrolment icons.
+ *
+ * @return array
+ */
+ public function get_enrolment_icons(): array {
+ if ($icons = enrol_get_course_info_icons($this->course)) {
+ return $icons;
+ }
+
+ return [];
+ }
+
+ /**
+ * Get the user progress in the course.
+ *
+ * @param null $userid
+ *
+ * @return mixed
+ */
+ public function get_progress($userid = null) {
+ return \core_completion\progress::get_course_progress_percentage(get_course($this->course->id), $userid);
+ }
+}
diff --git a/classes/util/user.php b/classes/util/user.php
new file mode 100644
index 00000000000..60b7f91d21c
--- /dev/null
+++ b/classes/util/user.php
@@ -0,0 +1,72 @@
+.
+
+namespace theme_boost_union\util;
+
+use stdClass;
+use user_picture;
+
+/**
+ * User class utility class
+ *
+ * @package theme_boost_union
+ * @copyright 2022 Willian Mano {@link https://conecti.me}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user {
+ /**
+ * @var \stdClass $user The user object.
+ */
+ protected $user;
+
+ /**
+ * Class constructor
+ *
+ * @param stdClass $user
+ *
+ */
+ public function __construct($user = null) {
+ global $USER, $DB;
+
+ if (!is_object($user)) {
+ $user = $DB->get_record('user', ['id' => $user], '*', MUST_EXIST);
+ }
+
+ if (!$user) {
+ $user = $USER;
+ }
+
+ $this->user = $user;
+ }
+
+ /**
+ * Returns the user picture
+ *
+ * @param int $imgsize
+ *
+ * @return \moodle_url
+ * @throws \coding_exception
+ */
+ public function get_user_picture($imgsize = 100) {
+ global $PAGE;
+
+ $userimg = new user_picture($this->user);
+
+ $userimg->size = $imgsize;
+
+ return $userimg->get_url($PAGE)->out();
+ }
+}
diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php
index 36c8caeb48b..528c88454a0 100644
--- a/lang/en/theme_boost_union.php
+++ b/lang/en/theme_boost_union.php
@@ -296,6 +296,18 @@
// ... ... Setting: Show course completion progress.
$string['courseoverviewshowprogresssetting'] = 'Show course completion progress';
$string['courseoverviewshowprogresssetting_desc'] = 'With this setting, you can control whether the course completion progress is visible inside the course overview block or not.';
+// ... ... Setting: Enable cards on course index.
+$string['enablecards'] = 'Enable cards on course index';
+$string['enablecardsdesc'] = 'Display courses as cards on site and course index.';
+// ... ... Setting: Enable course summary on course cards.
+$string['enablecardssummary'] = 'Enable summary on course cards';
+$string['enablecardssummarydesc'] = 'This setting controls were to hide or display the course summary in the course cards.';
+// ... ... Setting: Enable custom fields on course cards.
+$string['enablecardscfields'] = 'Enable custom fields on course cards';
+$string['enablecardscfieldsdesc'] = 'This setting controls were to hide or display the course custom fields in the course cards.';
+// ... ... Setting: Enable teachers pictures on course cards.
+$string['enableteacherspic'] = 'Enable teachers pictures';
+$string['enableteacherspicdesc'] = 'This setting controls were to hide or display the teachers\' pictures in the course cards.';
// Settings: Course tab.
$string['coursetab'] = 'Course';
diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss
index 19a1d601c54..2f29d8b0843 100644
--- a/scss/boost_union/post.scss
+++ b/scss/boost_union/post.scss
@@ -1,3 +1,114 @@
+.pagelayout-frontpage .dashboard-card-deck .dashboard-card,
+.pagelayout-coursecategory .dashboard-card-deck .dashboard-card {
+ width: 20%;
+}
+.pagelayout-frontpage .dashboard-card,
+.pagelayout-coursecategory .dashboard-card {
+ position: relative;
+ margin: 1rem;
+}
+.pagelayout-frontpage .dashboard-card.dimmed .card-img::after,
+.pagelayout-coursecategory .dashboard-card.dimmed .card-img::after {
+ content: "";
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0.5);
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ border-top-left-radius: 0.5rem;
+ border-top-right-radius: 0.5rem;
+}
+.pagelayout-frontpage .dashboard-card .dashboard-card-footer,
+.pagelayout-coursecategory .dashboard-card .dashboard-card-footer {
+ border-radius: 0.5rem;
+}
+.pagelayout-frontpage .dashboard-card .card-body,
+.pagelayout-coursecategory .dashboard-card .card-body {
+ padding: 0.8rem;
+}
+.pagelayout-frontpage .dashboard-card .customfields .customfield,
+.pagelayout-coursecategory .dashboard-card .customfields .customfield {
+ font-size: 80%;
+ margin-bottom: 0.25rem;
+}
+.pagelayout-frontpage .dashboard-card .customfields .customfield:last-of-type,
+.pagelayout-coursecategory .dashboard-card .customfields .customfield:last-of-type {
+ margin-bottom: 0;
+}
+.pagelayout-frontpage .dashboard-card .customfields,
+.pagelayout-coursecategory .dashboard-card .customfields {
+ margin-top: 1.5em;
+ margin-bottom: 1.5em;
+}
+.pagelayout-frontpage .dashboard-card .customfields .customfield .customfieldname,
+.pagelayout-frontpage .dashboard-card .customfields .customfield .customfieldseparator,
+.pagelayout-coursecategory .dashboard-card .customfields .customfield .customfieldname,
+.pagelayout-coursecategory .dashboard-card .customfields .customfield .customfieldseparator {
+ font-weight: 500;
+}
+.pagelayout-frontpage .dashboard-card .enrolmenticons,
+.pagelayout-coursecategory .dashboard-card .enrolmenticons {
+ position: absolute;
+ bottom: 8px;
+ right: 0;
+}
+.pagelayout-frontpage .dashboard-card .enrolmenticons .enrolmenticon,
+.pagelayout-coursecategory .dashboard-card .enrolmenticons .enrolmenticon {
+ padding: 4px 8px;
+ background-color: #fff;
+ color: #0f47ad;
+ border-radius: 0.5rem;
+ margin: 0 4px;
+}
+.pagelayout-frontpage .dashboard-card .enrolmenticons .enrolmenticon .icon,
+.pagelayout-coursecategory .dashboard-card .enrolmenticons .enrolmenticon .icon {
+ margin-right: 0;
+}
+.pagelayout-frontpage .dashboard-card .course-contacts .contact .info,
+.pagelayout-coursecategory .dashboard-card .course-contacts .contact .info {
+ margin-left: 1em;
+}
+.pagelayout-frontpage .dashboard-card .info .name,
+.pagelayout-coursecategory .dashboard-card .info .name {
+ margin-bottom: 0;
+}
+.pagelayout-frontpage .dashboard-card .course-contacts .contact,
+.pagelayout-coursecategory .dashboard-card .course-contacts .contact {
+ display: flex;
+}
+.pagelayout-frontpage .dashboard-card .course-contacts .contact:not(:first-of-type),
+.pagelayout-coursecategory .dashboard-card .course-contacts .contact:not(:first-of-type) {
+ margin-top: 0.4rem;
+}
+.pagelayout-frontpage .dashboard-card .course-contacts .contact img,
+.pagelayout-coursecategory .dashboard-card .course-contacts .contact img {
+ width: 36px;
+ height: 36px;
+ border: 1px solid #dee2e6;
+ margin-left: 1em;
+}
+.pagelayout-frontpage .dashboard-card .course-contacts .contact p.role,
+.pagelayout-coursecategory .dashboard-card .course-contacts .contact p.role {
+ color: #1d2125;
+ font-size: 80%;
+}
+.pagelayout-frontpage .dashboard-card .dashboard-card-footer,
+.pagelayout-coursecategory .dashboard-card .dashboard-card-footer {
+ padding: 0 0.8rem 0.8rem 0.8rem;
+}
+.pagelayout-frontpage .course_category_tree .category .categoryname.aabtn,
+.pagelayout-coursecategory .course_category_tree .category .categoryname.aabtn {
+ font-size: 1.5em;
+}
+
+.pagelayout-frontpage .dashboard-card .course-summary,
+.pagelayout-coursecategory .dashboard-card .course-summary {
+ max-height: 10em;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
/*=======================================
* Settings: Look -> Site branding
======================================*/
@@ -2585,7 +2696,6 @@ body.dir-rtl {
}
}
-
/*=======================================
* Supporting third-party plugins
======================================*/
@@ -2676,3 +2786,4 @@ body.dir-rtl {
}
}
}
+
diff --git a/settings.php b/settings.php
index 48ab4411c1c..55030b3ef2a 100644
--- a/settings.php
+++ b/settings.php
@@ -849,6 +849,42 @@
$setting->set_updatedcallback('theme_reset_all_caches');
$tab->add($setting);
+ // Enable cards on course index.
+ $name = 'theme_boost_union/enablecards';
+ $title = get_string('enablecards', 'theme_boost_union');
+ $description = get_string('enablecardsdesc', 'theme_boost_union');
+ $default = 0;
+ $choices = [0 => get_string('no'), 1 => get_string('yes')];
+ $setting = new admin_setting_configselect($name, $title, $description, $default, $choices);
+ $tab->add($setting);
+
+ // Enable summary on course cards.
+ $name = 'theme_boost_union/enablecardssummary';
+ $title = get_string('enablecardssummary', 'theme_boost_union');
+ $description = get_string('enablecardssummarydesc', 'theme_boost_union');
+ $default = 0;
+ $choices = [0 => get_string('no'), 1 => get_string('yes')];
+ $setting = new admin_setting_configselect($name, $title, $description, $default, $choices);
+ $tab->add($setting);
+
+ // Enable custom fields on course cards.
+ $name = 'theme_boost_union/enablecardscfields';
+ $title = get_string('enablecardscfields', 'theme_boost_union');
+ $description = get_string('enablecardscfieldsdesc', 'theme_boost_union');
+ $default = 0;
+ $choices = [0 => get_string('no'), 1 => get_string('yes')];
+ $setting = new admin_setting_configselect($name, $title, $description, $default, $choices);
+ $tab->add($setting);
+
+ // Enable teachers on course cards.
+ $name = 'theme_boost_union/enableteacherspic';
+ $title = get_string('enableteacherspic', 'theme_boost_union');
+ $description = get_string('enableteacherspicdesc', 'theme_boost_union');
+ $default = 0;
+ $choices = [0 => get_string('no'), 1 => get_string('yes')];
+ $setting = new admin_setting_configselect($name, $title, $description, $default, $choices);
+ $tab->add($setting);
+
// Add tab to settings page.
$page->add($tab);
diff --git a/templates/coursecard.mustache b/templates/coursecard.mustache
new file mode 100644
index 00000000000..406be6ef59d
--- /dev/null
+++ b/templates/coursecard.mustache
@@ -0,0 +1,57 @@
+