PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.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 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / TemplateHooks / CourseBuilder / Course / BuilderListCoursesTemplate.php

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

529 lines 17.6 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 if ( ! empty( $courses ) ) {
209 $html_courses = $this->list_courses( $courses );
210 } else {
211 $html_courses = Template::print_message(
212 sprintf( __( 'No courses found', 'learnpress' ) ),
213 'info',
214 false
215 );
216 }
217
218 $data_pagination = [
219 'paged' => $filter->page,
220 'total_pages' => DataBase::get_total_pages( $filter->limit, $total_courses ),
221 ];
222
223 $sections = apply_filters(
224 'learn-press/course-builder/courses/sections',
225 [
226 'wrapper' => '<div class="courses-builder__course-tab learn-press-courses">',
227 'courses' => $html_courses,
228 'pagination' => Template::instance()->html_pagination( $data_pagination ),
229 'wrapper_end' => '</div>',
230 ],
231 $courses,
232 $user
233 );
234
235 $content = Template::combine_components( $sections );
236 } catch ( Throwable $e ) {
237 error_log( __METHOD__ . ': ' . $e->getMessage() );
238 }
239
240 return $content;
241 }
242
243 /**
244 * Display list courses.
245 *
246 * @param $instructor
247 * @param $courses
248 *
249 * @return string
250 */
251 public function list_courses( $courses ): string {
252 $content = '';
253
254 try {
255 $html_list_course = '';
256 foreach ( $courses as $course_obj ) {
257 // Read fresh model to avoid stale post_status in long-lived cache,
258 // especially around future -> publish transitions.
259 $course = CourseModel::find( $course_obj->ID, false );
260 $html_list_course .= self::render_course( $course );
261 }
262
263 $sections = [
264 'wrapper' => '<ul class="cb-list-course">',
265 'list_course' => $html_list_course,
266 'wrapper_end' => '</ul>',
267 ];
268
269 $content = Template::combine_components( $sections );
270 } catch ( Throwable $e ) {
271 error_log( __METHOD__ . ': ' . $e->getMessage() );
272 }
273
274 return $content;
275 }
276
277 /**
278 * Render course in course builder
279 *
280 * @param CourseModel $courseModel
281 * @param array $settings
282 *
283 * @return string
284 * @since 4.3.6
285 * @version 1.0.0
286 */
287 public static function render_course( CourseModel $courseModel, array $settings = [] ): string {
288 $singleCourseTemplate = SingleCourseTemplate::instance();
289
290 try {
291 $course_id = $courseModel->get_id();
292 $course_status = $courseModel->get_status();
293 $edit_link = CourseBuilder::get_link_course_builder( CourseBuilderTemplate::MENU_COURSES . "/{$course_id}" );
294
295 // Offline badge overlay
296 $offline_badge = '';
297 if ( $courseModel->is_offline() ) {
298 $offline_badge = '<span class="cb-item-status-badge offline">Offline</span>';
299 }
300
301 $html_img = apply_filters(
302 'learn-press/course-builder/list-courses/item/section-top',
303 [
304 'wrapper' => '<div class="course-thumbnail">',
305 'link' => sprintf( '<a href="%s">', $edit_link ),
306 'img' => $singleCourseTemplate->html_image( $courseModel ),
307 'badge' => $offline_badge,
308 'link_end' => '</a>',
309 'wrapper_end' => '</div>',
310 ],
311 $courseModel,
312 $settings
313 );
314
315 // Icon mapping for meta items (using LearnPress frontend icons)
316 $meta_icons = [
317 'lesson' => '<i class="lp-icon-file-o"></i>',
318 'student' => '<i class="lp-icon-user-graduate"></i>',
319 'duration' => '<i class="lp-icon-clock-o"></i>',
320 'level' => '<i class="lp-icon-signal"></i>',
321 'quiz' => '<i class="lp-icon-puzzle-piece"></i>',
322 'address' => '<i class="lp-icon-map-marker"></i>',
323 ];
324
325 $meta_data = apply_filters(
326 'learn-press/course-builder/list-courses/item/meta-data',
327 [
328 'duration' => $singleCourseTemplate->html_duration( $courseModel ),
329 'level' => $singleCourseTemplate->html_level( $courseModel ),
330 'lesson' => $singleCourseTemplate->html_count_item( $courseModel, LP_LESSON_CPT ),
331 'quiz' => $singleCourseTemplate->html_count_item( $courseModel, LP_QUIZ_CPT ),
332 'student' => $singleCourseTemplate->html_count_student( $courseModel ),
333 ],
334 $courseModel,
335 $settings
336 );
337
338 if ( $courseModel->is_offline() ) {
339 $singleCourseOfflineTemplate = SingleCourseOfflineTemplate::instance();
340 unset( $meta_data['quiz'] );
341 unset( $meta_data['student'] );
342 if ( ! empty( $meta_data['lesson'] ) ) {
343 $meta_data['lesson'] = $singleCourseOfflineTemplate->html_lesson_info( $courseModel, true );
344 }
345
346 $html_address = $singleCourseOfflineTemplate->html_address( $courseModel );
347 if ( ! empty( $html_address ) ) {
348 $meta_data['address'] = $singleCourseOfflineTemplate->html_address( $courseModel );
349 }
350 }
351
352 $html_meta_data = '';
353 if ( ! empty( $meta_data ) ) {
354 foreach ( $meta_data as $k => $v ) {
355 $icon = $meta_icons[ $k ] ?? '';
356 $html_meta_data .= sprintf( '<div class="meta-item meta-item-%s">%s%s</div>', $k, $icon, $v );
357 }
358
359 $html_meta_data = sprintf( '<div class="course-wrap-meta">%s</div>', $html_meta_data );
360 }
361
362 $status_label = 'future' === $course_status ? __( 'Scheduled', 'learnpress' ) : $course_status;
363 $html_status = sprintf(
364 '<div class="course-status %1$s">
365 <span>%2$s</span>
366 </div>',
367 esc_attr( $course_status ),
368 esc_html( $status_label )
369 );
370
371 // Price
372 $html_price = sprintf( '<div class="course-item-price-wrap">%s</div>', $singleCourseTemplate->html_price( $courseModel ) );
373
374 // Categories
375 $html_categories = '';
376 $categories = $courseModel->get_categories();
377 if ( ! empty( $categories ) ) {
378 $cat_names = array();
379 foreach ( $categories as $cat ) {
380 $cat_name = is_object( $cat ) ? $cat->name : ( is_array( $cat ) ? ( $cat['name'] ?? '' ) : '' );
381 if ( ! empty( $cat_name ) ) {
382 $cat_names[] = sprintf( '<span class="course-category-name">%s</span>', esc_html( $cat_name ) );
383 }
384 }
385 if ( ! empty( $cat_names ) ) {
386 $html_categories = sprintf(
387 ' <span class="course-categories-label">%s</span> %s',
388 __( 'in', 'learnpress' ),
389 implode( ', ', $cat_names )
390 );
391 }
392 }
393
394 // Last Updated
395 $post_obj = get_post( $courseModel->get_id() );
396 $html_last_updated = '';
397 if ( $post_obj && ! empty( $post_obj->post_modified ) ) {
398 $modified_time = strtotime( $post_obj->post_modified );
399 $html_last_updated = sprintf(
400 '<div class="course-last-updated">%s %s</div>',
401 __( 'Last Updated on', 'learnpress' ),
402 date_i18n( 'Y/m/d \a\t g:i a', $modified_time )
403 );
404 }
405
406 $section_bottom_end = apply_filters(
407 'learn-press/course-builder/list-courses/item/section/bottom/end',
408 [
409 'wrapper' => '<div class="course-bottom">',
410 'price' => $html_price,
411 'status' => $html_status,
412 'last_updated' => $html_last_updated,
413 'wrapper_end' => '</div>',
414 ],
415 $courseModel,
416 $settings
417 );
418
419 // Instructor with categories
420 $instructor = $courseModel->get_author_model();
421 $instructor_name = $instructor ? $instructor->get_display_name() : '';
422 $html_instructor_category = '';
423 if ( ! empty( $instructor_name ) ) {
424 $html_instructor_category = sprintf(
425 '<div class="course-instructor-category"><div><span class="course-by-label">%s</span> <span class="instructor-display-name">%s</span>%s</div></div>',
426 __( 'by', 'learnpress' ),
427 esc_html( $instructor_name ),
428 $html_categories
429 );
430 }
431
432 $html_content = apply_filters(
433 'learn-press/course-builder/list-courses/item/section/bottom',
434 [
435 'wrapper' => '<div class="course-content">',
436 'title' => sprintf(
437 '<h3 class="wap-course-title"><a href="%s">%s</a></h3>',
438 $edit_link,
439 $singleCourseTemplate->html_title( $courseModel )
440 ),
441 'instructor' => $html_instructor_category,
442 'meta' => $html_meta_data,
443 'info' => Template::combine_components( $section_bottom_end ),
444 'wrapper_end' => '</div>',
445 ],
446 $courseModel,
447 $settings
448 );
449
450 $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
451
452 // Set action by status
453 $action_by_status = [];
454 if ( $course_status === PostModel::STATUS_TRASH ) {
455 $action_by_status['restore'] = __( 'Restore', 'learnpress' );
456 $action_by_status['delete'] = __( 'Delete permanently', 'learnpress' );
457 } else {
458 $action_by_status['duplicate'] = __( 'Duplicate', 'learnpress' );
459 $action_by_status[ PostModel::STATUS_PUBLISH ] = __( 'Publish', 'learnpress' );
460 $action_by_status[ PostModel::STATUS_PENDING ] = __( 'Pending Review', 'learnpress' );
461 $action_by_status[ PostModel::STATUS_DRAFT ] = __( 'Draft', 'learnpress' );
462 $action_by_status[ PostModel::STATUS_TRASH ] = __( 'Trash', 'learnpress' );
463 }
464 // Unset current status on action
465 unset( $action_by_status[ $course_status ] );
466
467 $html_action_by_status = '';
468
469 $data_send_action = [
470 'id_url' => 'cb-quick-edit-action',
471 'action' => 'cb_quick_edit_save_course',
472 'course_id' => $courseModel->get_id(),
473 ];
474
475 foreach ( $action_by_status as $action_status => $action_label ) {
476 $html_action_by_status .= sprintf(
477 '<span class="lp-cb-item-action %s" data-send="%s">%s</span>',
478 $action_status,
479 Template::convert_data_to_json( $data_send_action + [ 'action_type' => $action_status ] ),
480 $action_label
481 );
482 }
483
484 $html_action = apply_filters(
485 'learn-press/course-builder/list-courses/item/action',
486 [
487 'wrapper' => '<div class="lp-cb-item-action-wrap">',
488 'edit' => $course_status !== PostModel::STATUS_TRASH ? sprintf(
489 '<div class="course-action-editor"><a class="btn-edit-course course-edit-permalink" href="%s">%s</a></div>',
490 $edit_link,
491 __( 'Edit', 'learnpress' )
492 ) : '',
493 'action_expanded_button' => sprintf(
494 '<div class="lp-cb-item-action-expand-toggle lp-button">%s</div>',
495 $more_actions_icon
496 ),
497 'action_wrap' => '<div class="lp-cb-item-action-expand lp-hidden">',
498 'action' => $html_action_by_status,
499 'action_wrap_end' => '</div>',
500 'wrapper_end' => '</div>',
501 ],
502 $courseModel,
503 $settings
504 );
505
506 $section = apply_filters(
507 'learn-press/course-builder/list-courses/item-li',
508 [
509 'wrapper_li' => '<li class="course">',
510 'wrapper_div' => sprintf( '<div class="course-item" data-course-id="%s">', $courseModel->get_id() ),
511 'media' => Template::combine_components( $html_img ),
512 'course_info' => Template::combine_components( $html_content ),
513 'course_action' => Template::combine_components( $html_action ),
514 'wrapper_div_end' => '</div>',
515 'wrapper_li_end' => '</li>',
516 ],
517 $courseModel,
518 $settings
519 );
520
521 $html_item = Template::combine_components( $section );
522 } catch ( Throwable $e ) {
523 $html_item = $e->getMessage();
524 }
525
526 return $html_item;
527 }
528 }
529