PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 All 142 releases
learnpress / inc / TemplateHooks / CourseBuilder / Course / BuilderListCoursesTemplate.php

BuilderListCoursesTemplate.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.10, at inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php

530 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template hooks Tab Course in Course Builder.
4 *
5 * @since 4.3.6
6 * @version 1.0.0
7 */
8
9 namespace LearnPress\TemplateHooks\CourseBuilder\Course;
10
11 use LearnPress\CourseBuilder\CourseBuilder;
12 use LearnPress\Databases\DataBase;
13 use LearnPress\Helpers\Singleton;
14 use LearnPress\Helpers\Template;
15 use LearnPress\Models\CourseModel;
16 use LearnPress\Models\Courses;
17 use LearnPress\Models\PostModel;
18 use LearnPress\Models\UserModel;
19 use LearnPress\Services\OpenAiService;
20 use LearnPress\TemplateHooks\Admin\AI\AdminCreateCourseAITemplate;
21 use LearnPress\TemplateHooks\Course\SingleCourseOfflineTemplate;
22 use LearnPress\TemplateHooks\Course\SingleCourseTemplate;
23 use LearnPress\TemplateHooks\CourseBuilder\CourseBuilderTemplate;
24 use LP_Course_Filter;
25 use LP_WP_Filesystem;
26 use Throwable;
27
28 class BuilderListCoursesTemplate {
29 use Singleton;
30
31 public function init() {}
32
33 /**
34 * HTML list courses on Course Builder screen
35 *
36 * @param array $data
37 *
38 * @since 4.3.6
39 * @version 1.0.1
40 * @return string
41 */
42 public function layout( array $data = [] ): string {
43 $section = [
44 'header' => $this->html_header( $data ),
45 'filter_bar' => $this->html_filter_bar(),
46 'courses' => $this->tab_list_courses(),
47 'ai_templates' => AdminCreateCourseAITemplate::instance()->render_for_frontend(),
48 ];
49
50 return Template::combine_components( $section );
51 }
52
53 /**
54 * HTML header
55 *
56 * @param array $data
57 *
58 * @return string
59 */
60 public function html_header( array $data = [] ): string {
61 $btn_add_new = sprintf(
62 '<a href="%s" class="cb-btn-add-new lp-button">%s</a>',
63 esc_url( CourseBuilder::get_link_course_builder( 'courses/create' ) ),
64 __( 'Add New Course', 'learnpress' )
65 );
66
67 $enable_open_ai = OpenAiService::instance()->is_enable()
68 && ! empty( OpenAiService::instance()->get_secret_key() );
69 $ai_btn_class = $enable_open_ai ? 'lp-btn-generate-course-with-ai' : 'lp-btn-warning-enable-ai';
70 $btn_generate_ai = sprintf(
71 '<button type="button" class="cb-btn-add-new %s">
72 <i class="lp-ico-ai"></i> %s
73 </button>',
74 esc_attr( $ai_btn_class ),
75 esc_html__( 'Generate with AI', 'learnpress' )
76 );
77
78 $header = [
79 'wrapper' => '<div class="cb-tab-header">',
80 'title' => sprintf( '<h2 class="lp-cb-tab__title">%s</h2>', __( 'Courses', 'learnpress' ) ),
81 'actions' => sprintf(
82 '<div class="cb-tab-header-actions" style="display:flex;align-items:center;gap:8px;">%s%s</div>',
83 $btn_add_new,
84 $btn_generate_ai
85 ),
86 'wrapper_end' => '</div>',
87 ];
88
89 return Template::combine_components( $header );
90 }
91
92 /**
93 * Render filter bar with search, status, items per page dropdowns
94 *
95 * @return string
96 * @since 4.3.0
97 */
98 public function html_filter_bar(): string {
99 $args = lp_archive_skeleton_get_args();
100 $link_tab = CourseBuilder::get_tab_link( 'courses' );
101
102 // Current filter values
103 $current_search = $args['c_search'] ?? '';
104 $current_status = $args['c_status'] ?? '';
105 $current_limit = $args['per_page'] ?? 20;
106
107 // Status options
108 $statuses = [
109 '' => __( 'All Status', 'learnpress' ),
110 'publish' => __( 'Published', 'learnpress' ),
111 'future' => __( 'Scheduled', 'learnpress' ),
112 'draft' => __( 'Draft', 'learnpress' ),
113 'pending' => __( 'Pending Review', 'learnpress' ),
114 'private' => __( 'Private', 'learnpress' ),
115 'trash' => __( 'Trash', 'learnpress' ),
116 ];
117
118 // Items per page options
119 $per_page_options = [ 10, 20, 50 ];
120
121 // Build status dropdown HTML
122 $status_options = '';
123 foreach ( $statuses as $value => $label ) {
124 $selected = ( $current_status === $value ) ? 'selected' : '';
125 $status_options .= sprintf( '<option value="%s" %s>%s</option>', esc_attr( $value ), $selected, esc_html( $label ) );
126 }
127
128 // Build per page dropdown HTML
129 $per_page_html = '';
130 foreach ( $per_page_options as $option ) {
131 $selected = ( (int) $current_limit === $option ) ? 'selected' : '';
132 $per_page_html .= sprintf( '<option value="%d" %s>%d</option>', $option, $selected, $option );
133 }
134
135 // Row 2: Filter toolbar
136 $filter = [
137 'wrapper' => sprintf( '<form class="cb-tab-filter-bar" method="get" action="%s">', esc_url( $link_tab ) ),
138 'fields' => '<div class="cb-filter-fields">',
139 'search' => sprintf(
140 '<div class="cb-filter-group">
141 <label>%s</label>
142 <div class="cb-filter-search">
143 <input type="search" name="c_search" placeholder="%s" value="%s">
144 </div>
145 </div>',
146 esc_html__( 'Search', 'learnpress' ),
147 esc_attr__( 'Search by title', 'learnpress' ),
148 esc_attr( $current_search )
149 ),
150 'status' => sprintf(
151 '<div class="cb-filter-group">
152 <label>%s</label>
153 <select name="c_status" class="cb-filter-select">%s</select>
154 </div>',
155 esc_html__( 'Status', 'learnpress' ),
156 $status_options
157 ),
158 'per_page' => sprintf(
159 '<div class="cb-filter-group">
160 <label>%s</label>
161 <select name="per_page" class="cb-filter-select">%s</select>
162 </div>',
163 esc_html__( 'Items per page', 'learnpress' ),
164 $per_page_html
165 ),
166 'actions' => '<div class="cb-filter-actions">',
167 'filter_btn' => sprintf( '<button type="submit" class="cb-filter-btn">%s</button>', __( 'Filter', 'learnpress' ) ),
168 'reset_btn' => sprintf( '<a href="%s" class="cb-filter-reset">%s</a>', esc_url( $link_tab ), __( 'Reset', 'learnpress' ) ),
169 'actions_end' => '</div>',
170 'fields_end' => '</div>',
171 'wrapper_end' => '</form>',
172 ];
173
174 return Template::combine_components( $filter );
175 }
176
177 public function tab_list_courses(): string {
178 $content = '';
179
180 try {
181 $user = UserModel::find( get_current_user_id(), true );
182 // Query courses of user
183 $filter = new LP_Course_Filter();
184 $param = lp_archive_skeleton_get_args();
185 $param['id_url'] = 'tab-list-courses';
186
187 // Handle status filter - if empty, show all; otherwise filter by selected status
188 $status_param = $param['c_status'] ?? '';
189 if ( empty( $status_param ) ) {
190 $param['c_status'] = 'publish,future,private,draft,pending,trash';
191 }
192
193 Courses::handle_params_for_query_courses( $filter, $param );
194 // Admin can view all courses in Course Builder.
195 if ( ! current_user_can( ADMIN_ROLE ) ) {
196 $filter->post_author = $user->get_id();
197 }
198
199 // Handle per_page parameter
200 $per_page = isset( $param['per_page'] ) ? absint( $param['per_page'] ) : 20;
201 $filter->limit = $per_page > 0 ? $per_page : 20;
202
203 $filter->page = $GLOBALS['wp_query']->get( 'paged', 1 ) ? $GLOBALS['wp_query']->get( 'paged', 1 ) : 1;
204 $filter = apply_filters( 'lp/course-builder/courses/query/filter', $filter, [] );
205
206 $total_courses = 0;
207 $courses = Courses::get_courses( $filter, $total_courses );
208 $html_courses = $this->list_courses( $courses );
209
210 $data_pagination = [
211 'paged' => $filter->page,
212 'total_pages' => DataBase::get_total_pages( $filter->limit, $total_courses ),
213 ];
214
215 $sections = apply_filters(
216 'learn-press/course-builder/courses/sections',
217 [
218 'wrapper' => '<div class="courses-builder__course-tab learn-press-courses">',
219 'courses' => $html_courses,
220 'pagination' => Template::instance()->html_pagination( $data_pagination ),
221 'wrapper_end' => '</div>',
222 ],
223 $courses,
224 $user
225 );
226
227 $content = Template::combine_components( $sections );
228 } catch ( Throwable $e ) {
229 error_log( __METHOD__ . ': ' . $e->getMessage() );
230 }
231
232 return $content;
233 }
234
235 /**
236 * Display list courses.
237 *
238 * @param $instructor
239 * @param $courses
240 *
241 * @return string
242 */
243 public function list_courses( $courses ): string {
244 $content = '';
245
246 try {
247 $html_list_course = '';
248
249 if ( empty( $courses ) ) {
250 return Template::print_message(
251 __( 'No courses found', 'learnpress' ),
252 'info',
253 false
254 );
255 }
256
257 foreach ( $courses as $course_obj ) {
258 // Read fresh model to avoid stale post_status in long-lived cache,
259 // especially around future -> publish transitions.
260 $course = CourseModel::find( $course_obj->ID, false );
261 $html_list_course .= self::render_course( $course );
262 }
263
264 $sections = [
265 'wrapper' => '<ul class="cb-list-course">',
266 'list_course' => $html_list_course,
267 'wrapper_end' => '</ul>',
268 ];
269
270 $content = Template::combine_components( $sections );
271 } catch ( Throwable $e ) {
272 error_log( __METHOD__ . ': ' . $e->getMessage() );
273 }
274
275 return $content;
276 }
277
278 /**
279 * Render course in course builder
280 *
281 * @param CourseModel $courseModel
282 * @param array $settings
283 *
284 * @return string
285 * @since 4.3.6
286 * @version 1.0.0
287 */
288 public static function render_course( CourseModel $courseModel, array $settings = [] ): string {
289 $singleCourseTemplate = SingleCourseTemplate::instance();
290
291 try {
292 $course_id = $courseModel->get_id();
293 $course_status = $courseModel->get_status();
294 $edit_link = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_COURSES . "/{$course_id}" );
295
296 // Offline badge overlay
297 $offline_badge = '';
298 if ( $courseModel->is_offline() ) {
299 $offline_badge = '<span class="cb-item-status-badge offline">Offline</span>';
300 }
301
302 $html_img = apply_filters(
303 'learn-press/course-builder/list-courses/item/section-top',
304 [
305 'wrapper' => '<div class="course-thumbnail">',
306 'link' => sprintf( '<a href="%s">', $edit_link ),
307 'img' => $singleCourseTemplate->html_image( $courseModel ),
308 'badge' => $offline_badge,
309 'link_end' => '</a>',
310 'wrapper_end' => '</div>',
311 ],
312 $courseModel,
313 $settings
314 );
315
316 // Icon mapping for meta items (using LearnPress frontend icons)
317 $meta_icons = [
318 'lesson' => '<i class="lp-icon-file-o"></i>',
319 'student' => '<i class="lp-icon-user-graduate"></i>',
320 'duration' => '<i class="lp-icon-clock-o"></i>',
321 'level' => '<i class="lp-icon-signal"></i>',
322 'quiz' => '<i class="lp-icon-puzzle-piece"></i>',
323 'address' => '<i class="lp-icon-map-marker"></i>',
324 ];
325
326 $meta_data = apply_filters(
327 'learn-press/course-builder/list-courses/item/meta-data',
328 [
329 'duration' => $singleCourseTemplate->html_duration( $courseModel ),
330 'level' => $singleCourseTemplate->html_level( $courseModel ),
331 'lesson' => $singleCourseTemplate->html_count_item( $courseModel, LP_LESSON_CPT ),
332 'quiz' => $singleCourseTemplate->html_count_item( $courseModel, LP_QUIZ_CPT ),
333 'student' => $singleCourseTemplate->html_count_student( $courseModel ),
334 ],
335 $courseModel,
336 $settings
337 );
338
339 if ( $courseModel->is_offline() ) {
340 $singleCourseOfflineTemplate = SingleCourseOfflineTemplate::instance();
341 unset( $meta_data['quiz'] );
342 unset( $meta_data['student'] );
343 if ( ! empty( $meta_data['lesson'] ) ) {
344 $meta_data['lesson'] = $singleCourseOfflineTemplate->html_lesson_info( $courseModel, true );
345 }
346
347 $html_address = $singleCourseOfflineTemplate->html_address( $courseModel );
348 if ( ! empty( $html_address ) ) {
349 $meta_data['address'] = $singleCourseOfflineTemplate->html_address( $courseModel );
350 }
351 }
352
353 $html_meta_data = '';
354 if ( ! empty( $meta_data ) ) {
355 foreach ( $meta_data as $k => $v ) {
356 $icon = $meta_icons[ $k ] ?? '';
357 $html_meta_data .= sprintf( '<div class="meta-item meta-item-%s">%s%s</div>', $k, $icon, $v );
358 }
359
360 $html_meta_data = sprintf( '<div class="course-wrap-meta">%s</div>', $html_meta_data );
361 }
362
363 $status_label = 'future' === $course_status ? __( 'Scheduled', 'learnpress' ) : $course_status;
364 $html_status = sprintf(
365 '<div class="course-status %1$s">
366 <span>%2$s</span>
367 </div>',
368 esc_attr( $course_status ),
369 esc_html( $status_label )
370 );
371
372 // Price
373 $html_price = sprintf( '<div class="course-item-price-wrap">%s</div>', $singleCourseTemplate->html_price( $courseModel ) );
374
375 // Categories
376 $html_categories = '';
377 $categories = $courseModel->get_categories();
378 if ( ! empty( $categories ) ) {
379 $cat_names = array();
380 foreach ( $categories as $cat ) {
381 $cat_name = is_object( $cat ) ? $cat->name : ( is_array( $cat ) ? ( $cat['name'] ?? '' ) : '' );
382 if ( ! empty( $cat_name ) ) {
383 $cat_names[] = sprintf( '<span class="course-category-name">%s</span>', esc_html( $cat_name ) );
384 }
385 }
386 if ( ! empty( $cat_names ) ) {
387 $html_categories = sprintf(
388 ' <span class="course-categories-label">%s</span> %s',
389 __( 'in', 'learnpress' ),
390 implode( ', ', $cat_names )
391 );
392 }
393 }
394
395 // Last Updated
396 $post_obj = get_post( $courseModel->get_id() );
397 $html_last_updated = '';
398 if ( $post_obj && ! empty( $post_obj->post_modified ) ) {
399 $modified_time = strtotime( $post_obj->post_modified );
400 $html_last_updated = sprintf(
401 '<div class="course-last-updated">%s %s</div>',
402 __( 'Last Updated on', 'learnpress' ),
403 date_i18n( 'Y/m/d \a\t g:i a', $modified_time )
404 );
405 }
406
407 $section_bottom_end = apply_filters(
408 'learn-press/course-builder/list-courses/item/section/bottom/end',
409 [
410 'wrapper' => '<div class="course-bottom">',
411 'price' => $html_price,
412 'status' => $html_status,
413 'last_updated' => $html_last_updated,
414 'wrapper_end' => '</div>',
415 ],
416 $courseModel,
417 $settings
418 );
419
420 // Instructor with categories
421 $instructor = $courseModel->get_author_model();
422 $instructor_name = $instructor ? $instructor->get_display_name() : '';
423 $html_instructor_category = '';
424 if ( ! empty( $instructor_name ) ) {
425 $html_instructor_category = sprintf(
426 '<div class="course-instructor-category"><div><span class="course-by-label">%s</span> <span class="instructor-display-name">%s</span>%s</div></div>',
427 __( 'by', 'learnpress' ),
428 esc_html( $instructor_name ),
429 $html_categories
430 );
431 }
432
433 $html_content = apply_filters(
434 'learn-press/course-builder/list-courses/item/section/bottom',
435 [
436 'wrapper' => '<div class="course-content">',
437 'title' => sprintf(
438 '<h3 class="wap-course-title"><a href="%s">%s</a></h3>',
439 $edit_link,
440 $singleCourseTemplate->html_title( $courseModel )
441 ),
442 'instructor' => $html_instructor_category,
443 'meta' => $html_meta_data,
444 'info' => Template::combine_components( $section_bottom_end ),
445 'wrapper_end' => '</div>',
446 ],
447 $courseModel,
448 $settings
449 );
450
451 $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
452
453 // Set action by status
454 $action_by_status = [];
455 if ( $course_status === PostModel::STATUS_TRASH ) {
456 $action_by_status['restore'] = __( 'Restore', 'learnpress' );
457 $action_by_status['delete'] = __( 'Delete permanently', 'learnpress' );
458 } else {
459 $action_by_status['duplicate'] = __( 'Duplicate', 'learnpress' );
460 $action_by_status[ PostModel::STATUS_PUBLISH ] = __( 'Publish', 'learnpress' );
461 $action_by_status[ PostModel::STATUS_PENDING ] = __( 'Pending Review', 'learnpress' );
462 $action_by_status[ PostModel::STATUS_DRAFT ] = __( 'Draft', 'learnpress' );
463 $action_by_status[ PostModel::STATUS_TRASH ] = __( 'Trash', 'learnpress' );
464 }
465 // Unset current status on action
466 unset( $action_by_status[ $course_status ] );
467
468 $html_action_by_status = '';
469
470 $data_send_action = [
471 'id_url' => 'cb-quick-edit-action',
472 'action' => 'cb_quick_edit_save_course',
473 'course_id' => $courseModel->get_id(),
474 ];
475
476 foreach ( $action_by_status as $action_status => $action_label ) {
477 $html_action_by_status .= sprintf(
478 '<span class="lp-cb-item-action %s" data-send="%s">%s</span>',
479 $action_status,
480 Template::convert_data_to_json( $data_send_action + [ 'action_type' => $action_status ] ),
481 $action_label
482 );
483 }
484
485 $html_action = apply_filters(
486 'learn-press/course-builder/list-courses/item/action',
487 [
488 'wrapper' => '<div class="lp-cb-item-action-wrap">',
489 'edit' => $course_status !== PostModel::STATUS_TRASH ? sprintf(
490 '<div class="course-action-editor"><a class="btn-edit-course course-edit-permalink" href="%s">%s</a></div>',
491 $edit_link,
492 __( 'Edit', 'learnpress' )
493 ) : '',
494 'action_expanded_button' => sprintf(
495 '<div class="lp-cb-item-action-expand-toggle lp-button">%s</div>',
496 $more_actions_icon
497 ),
498 'action_wrap' => '<div class="lp-cb-item-action-expand lp-hidden">',
499 'action' => $html_action_by_status,
500 'action_wrap_end' => '</div>',
501 'wrapper_end' => '</div>',
502 ],
503 $courseModel,
504 $settings
505 );
506
507 $section = apply_filters(
508 'learn-press/course-builder/list-courses/item-li',
509 [
510 'wrapper_li' => '<li class="course">',
511 'wrapper_div' => sprintf( '<div class="course-item" data-course-id="%s">', $courseModel->get_id() ),
512 'media' => Template::combine_components( $html_img ),
513 'course_info' => Template::combine_components( $html_content ),
514 'course_action' => Template::combine_components( $html_action ),
515 'wrapper_div_end' => '</div>',
516 'wrapper_li_end' => '</li>',
517 ],
518 $courseModel,
519 $settings
520 );
521
522 $html_item = Template::combine_components( $section );
523 } catch ( Throwable $e ) {
524 $html_item = $e->getMessage();
525 }
526
527 return $html_item;
528 }
529 }
530