init_hooks(); } /** * Initialize hooks. */ private function init_hooks(): void { // AJAX actions for both logged in and non-logged in users add_action('wp_ajax_king_addons_dynamic_posts_grid_filter', [$this, 'handle_filter_request']); add_action('wp_ajax_nopriv_king_addons_dynamic_posts_grid_filter', [$this, 'handle_filter_request']); add_action('wp_ajax_king_addons_dynamic_posts_grid_load_more', [$this, 'handle_load_more_request']); add_action('wp_ajax_nopriv_king_addons_dynamic_posts_grid_load_more', [$this, 'handle_load_more_request']); // Enqueue AJAX variables add_action('wp_enqueue_scripts', [$this, 'enqueue_ajax_variables']); } /** * Enqueue AJAX variables for frontend. */ public function enqueue_ajax_variables(): void { wp_localize_script('jquery', 'KingAddonsDynamicPostsGrid', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('king_addons_dynamic_posts_grid_nonce'), ]); } /** * Handle filter AJAX request. */ public function handle_filter_request(): void { try { // Verify nonce if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) { wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]); } // Get and sanitize POST data $widget_id = sanitize_text_field($_POST['widget_id'] ?? ''); $posts_per_page = absint($_POST['posts_per_page'] ?? 12); // Handle post_types - it comes as JSON string from JavaScript or as array $post_types_raw = $_POST['post_types'] ?? '["post"]'; // Debug: log the type and value of post_types_raw // error_log('DEBUG: post_types_raw type: ' . gettype($post_types_raw) . ', value: ' . print_r($post_types_raw, true)); // Check if it's already an array (PRO version) or needs decoding (Free version) if (is_array($post_types_raw)) { $post_types = $this->sanitize_array($post_types_raw); } else { // Ensure it's a string before decoding if (!is_string($post_types_raw)) { // error_log('DEBUG: post_types_raw is not a string, converting to: ' . json_encode($post_types_raw)); $post_types_raw = json_encode($post_types_raw); } $post_types_decoded = json_decode($post_types_raw, true); $post_types = is_array($post_types_decoded) ? $this->sanitize_array($post_types_decoded) : ['post']; } $orderby = sanitize_text_field($_POST['orderby'] ?? 'date'); $order = sanitize_text_field($_POST['order'] ?? 'DESC'); $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); $search_query = sanitize_text_field($_POST['search_query'] ?? ''); $page = absint($_POST['page'] ?? 1); $cpt_actions_raw = wp_unslash($_POST['cpt_actions'] ?? ''); $cpt_actions = $this->sanitize_cpt_actions($cpt_actions_raw); $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1'; // Build query arguments $query_args = $this->build_query_args([ 'posts_per_page' => $posts_per_page, 'post_types' => $post_types, 'orderby' => $orderby, 'order' => $order, 'filter_taxonomy' => $filter_taxonomy, 'filter_term' => $filter_term, 'search_query' => $search_query, 'page' => $page, ]); // Execute query $posts_query = new \WP_Query($query_args); // Generate posts HTML (use CPT classing when filtering by post_type) $is_cpt_mode = ($filter_taxonomy === 'post_type'); $posts_html = $this->generate_posts_html($posts_query, $show_excerpt, $is_cpt_mode, $cpt_actions); // Prepare response data $response_data = [ 'posts_html' => $posts_html, 'current_page' => $page, 'max_pages' => $posts_query->max_num_pages, 'total_posts' => $posts_query->found_posts, 'current_count' => $posts_query->post_count, ]; wp_send_json_success($response_data); } catch (Exception $e) { // error_log('Dynamic Posts Grid Filter AJAX Error: ' . $e->getMessage()); // error_log('Stack trace: ' . $e->getTraceAsString()); wp_send_json_error(['message' => esc_html__('An error occurred while filtering posts.', 'king-addons')]); } } /** * Handle load more AJAX request. */ public function handle_load_more_request(): void { try { // Verify nonce if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) { wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]); } // Get and sanitize POST data $widget_id = sanitize_text_field($_POST['widget_id'] ?? ''); $posts_per_page = absint($_POST['posts_per_page'] ?? 12); // Handle post_types - it comes as JSON string from JavaScript or as array $post_types_raw = $_POST['post_types'] ?? '["post"]'; // Debug: log the type and value of post_types_raw // error_log('DEBUG: post_types_raw type: ' . gettype($post_types_raw) . ', value: ' . print_r($post_types_raw, true)); // Check if it's already an array (PRO version) or needs decoding (Free version) if (is_array($post_types_raw)) { $post_types = $this->sanitize_array($post_types_raw); } else { // Ensure it's a string before decoding if (!is_string($post_types_raw)) { // error_log('DEBUG: post_types_raw is not a string, converting to: ' . json_encode($post_types_raw)); $post_types_raw = json_encode($post_types_raw); } $post_types_decoded = json_decode($post_types_raw, true); $post_types = is_array($post_types_decoded) ? $this->sanitize_array($post_types_decoded) : ['post']; } $orderby = sanitize_text_field($_POST['orderby'] ?? 'date'); $order = sanitize_text_field($_POST['order'] ?? 'DESC'); $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); $search_query = sanitize_text_field($_POST['search_query'] ?? ''); $page = absint($_POST['page'] ?? 1); $cpt_actions_raw = wp_unslash($_POST['cpt_actions'] ?? ''); $cpt_actions = $this->sanitize_cpt_actions($cpt_actions_raw); $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1'; // Build query arguments for load more (accumulative) $query_args = $this->build_query_args([ 'posts_per_page' => $posts_per_page, 'post_types' => $post_types, 'orderby' => $orderby, 'order' => $order, 'filter_taxonomy' => $filter_taxonomy, 'filter_term' => $filter_term, 'search_query' => $search_query, 'page' => $page, ]); // Execute query $posts_query = new \WP_Query($query_args); // Generate posts HTML (use CPT classing when filtering by post_type) $is_cpt_mode = ($filter_taxonomy === 'post_type'); $posts_html = $this->generate_posts_html($posts_query, $show_excerpt, $is_cpt_mode, $cpt_actions); // Calculate total shown posts (previous pages + current page) $total_shown = (($page - 1) * $posts_per_page) + $posts_query->post_count; // Prepare response data $response_data = [ 'posts_html' => $posts_html, 'current_page' => $page, 'max_pages' => $posts_query->max_num_pages, 'total_posts' => $posts_query->found_posts, 'current_count' => $total_shown, ]; wp_send_json_success($response_data); } catch (Exception $e) { // error_log('Dynamic Posts Grid AJAX Error: ' . $e->getMessage()); // error_log('Stack trace: ' . $e->getTraceAsString()); wp_send_json_error(['message' => esc_html__('An error occurred while loading posts.', 'king-addons')]); } } /** * Build WP_Query arguments. * * @param array $params Query parameters. * @return array WP_Query arguments. */ private function build_query_args(array $params): array { $query_args = [ 'post_type' => $params['post_types'], 'posts_per_page' => $params['posts_per_page'], 'paged' => $params['page'], 'orderby' => $params['orderby'], 'order' => $params['order'], 'post_status' => 'publish', 'ignore_sticky_posts' => true, ]; // Add search query if (!empty($params['search_query'])) { $query_args['s'] = $params['search_query']; } // Add taxonomy filter if (!empty($params['filter_term']) && $params['filter_term'] !== '*') { $query_args['tax_query'] = [ [ 'taxonomy' => $params['filter_taxonomy'], 'field' => 'slug', 'terms' => $params['filter_term'], ] ]; } // Handle random order if ($params['orderby'] === 'rand') { $query_args['orderby'] = 'rand'; unset($query_args['order']); // Order is not applicable for random } return $query_args; } /** * Generate posts HTML. * * @param \WP_Query $posts_query WP_Query object. * @param bool $show_excerpt Whether to show excerpt. * @return string Generated HTML. */ private function generate_posts_html(\WP_Query $posts_query, bool $show_excerpt = true, bool $is_cpt_mode = false, array $cpt_actions = []): string { if (!$posts_query->have_posts()) { return '
' . esc_html__('No posts found.', 'king-addons') . '
'; } ob_start(); while ($posts_query->have_posts()): $posts_query->the_post(); $post = get_post(); $cta_text = $this->get_cta_text($post); if ($is_cpt_mode) { $post_type = $post->post_type; $card_classes = 'king-addons-dpg-cpt-' . $post_type; $post_type_obj = get_post_type_object($post_type); $post_type_display = $post_type_obj ? strtoupper($post_type_obj->label) : strtoupper($post_type); $post_icon = ''; } else { $post_type_display = $this->get_post_type_display($post); $card_classes = $this->get_post_category_color_classes($post); $post_icon = $this->get_post_type_icon($post); } ?>
>

post_type; $action_conf = $cpt_actions[$pt] ?? []; $url_field = $action_conf['url_field'] ?? ''; $action_type = $action_conf['action_type'] ?? ''; $cta_override = $action_conf['cta_text'] ?? ''; $meta_url = $url_field ? get_post_meta($post->ID, $url_field, true) : ''; ?>
ID, $taxonomy); if ($terms && !is_wp_error($terms)) { // Use first category name $term = $terms[0]; return strtoupper($term->name); } // Fallback to post format/type based display $format = get_post_format($post->ID); if ($format === 'video') { return 'VIDEO'; } elseif ($format === 'audio') { return 'PUBLICATION'; } elseif ($post->post_type === 'page') { return 'WEBSITE'; } else { return 'POST'; } } /** * Get CTA button text (fallback for AJAX context). * * @param object $post Post object. * @return string CTA button text. */ private function get_cta_text($post): string { // Simple fallback for AJAX - smart detection only $taxonomy = 'category'; $terms = get_the_terms($post->ID, $taxonomy); if ($terms && !is_wp_error($terms)) { $term = $terms[0]; $slug = strtolower($term->slug); // Basic smart mapping for AJAX $smart_cta_map = [ 'video' => 'WATCH NOW', 'videos' => 'WATCH NOW', 'website' => 'LEARN MORE', 'websites' => 'LEARN MORE', ]; if (isset($smart_cta_map[$slug])) { return $smart_cta_map[$slug]; } } return 'READ MORE'; } /** * Get post category color classes. * * @param object $post Post object. * @return string Color class names. */ private function get_post_category_color_classes($post): string { $classes = []; $taxonomy = 'category'; // Default to category for AJAX requests // Get post categories/terms $terms = get_the_terms($post->ID, $taxonomy); if ($terms && !is_wp_error($terms)) { foreach ($terms as $term) { $classes[] = 'filter-' . $term->slug; $classes[] = 'king-addons-dpg-category-' . $term->slug; } } // Add default class if no terms if (empty($classes)) { $classes[] = 'king-addons-dpg-category-default'; } return implode(' ', $classes); } /** * Get post type icon (fallback icons for AJAX context). * * @param object $post Post object. * @return string Icon HTML. */ private function get_post_type_icon($post): string { // Get post categories/terms $taxonomy = 'category'; $terms = get_the_terms($post->ID, $taxonomy); if ($terms && !is_wp_error($terms)) { // Use first category to determine icon type $term = $terms[0]; // Map common category names to icons $category_icon_map = [ 'report' => '', 'reports' => '', 'publication' => '', 'publications' => '', 'video' => '', 'videos' => '', 'website' => '', 'websites' => '', 'news' => '', 'blog' => '', 'article' => '', 'articles' => '', ]; $slug = strtolower($term->slug); if (isset($category_icon_map[$slug])) { return $category_icon_map[$slug]; } } // Fallback based on post format $format = get_post_format($post->ID); if ($format === 'video') { return ''; } elseif ($format === 'audio') { return ''; } elseif ($post->post_type === 'page') { return ''; } else { return ''; } } /** * Sanitize CPT actions configuration JSON. * * @param string $raw_json Raw JSON from request. * @return array Sanitized CPT actions config. */ private function sanitize_cpt_actions(string $raw_json): array { if (empty($raw_json)) { return []; } $decoded = json_decode($raw_json, true); if (!is_array($decoded)) { return []; } $sanitized = []; foreach ($decoded as $pt => $conf) { $pt_key = sanitize_key($pt); if (!$pt_key) { continue; } $sanitized[$pt_key] = [ 'url_field' => isset($conf['url_field']) ? sanitize_key($conf['url_field']) : '', 'action_type' => isset($conf['action_type']) ? sanitize_text_field($conf['action_type']) : '', 'cta_text' => isset($conf['cta_text']) ? sanitize_text_field($conf['cta_text']) : '', ]; } return $sanitized; } /** * Sanitize array values. * * @param array $array Array to sanitize. * @return array Sanitized array. */ private function sanitize_array(array $array): array { return array_map('sanitize_text_field', $array); } }