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
← All changes | inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php +529 -528 4.3.9.1 → 4.4.10 View file →
@@ -1,528 +1,529 @@
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 -}
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 +}