PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
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 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
← All changes | inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php +528 -528 4.3.94.4.8 View file →
@@ -1,528 +1,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 - 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 + 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 +}