6, 'show_featured_image' => true, 'image_size' => 'medium', 'show_excerpt' => true, 'excerpt_length' => 20, 'show_read_more' => true, 'read_more_text' => 'Read More', 'sort_by' => 'date', 'sort_order' => 'DESC', 'categories' => '', 'tags' => '', 'authors' => '', 'date_range' => '', 'highlight_new' => false, 'new_post_days' => 7, 'show_search' => true, 'show_filters' => true, 'layout' => 'grid', // grid or list 'ajax_pagination' => true, 'load_more_text' => 'Load More Posts' ); /** * Constructor * * @param AISTMA_Plugin $plugin Plugin instance. */ public function __construct( $plugin ) { $this->plugin = $plugin; $this->init_hooks(); } /** * Initialize hooks */ private function init_hooks() { add_action( 'init', array( $this, 'register_shortcode' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_aistma_posts_gadget_load', array( $this, 'ajax_load_posts' ) ); add_action( 'wp_ajax_nopriv_aistma_posts_gadget_load', array( $this, 'ajax_load_posts' ) ); add_action( 'wp_ajax_aistma_posts_gadget_search', array( $this, 'ajax_search_posts' ) ); add_action( 'wp_ajax_nopriv_aistma_posts_gadget_search', array( $this, 'ajax_search_posts' ) ); } /** * Register the posts gadget shortcode */ public function register_shortcode() { add_shortcode( 'aistma_posts_gadget', array( $this, 'render_shortcode' ) ); } /** * Enqueue frontend assets */ public function enqueue_assets() { // Only enqueue on pages that use the shortcode global $post; if ( is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'aistma_posts_gadget' ) ) { wp_enqueue_style( 'aistma-posts-gadget', AISTMA_URL . 'public/css/posts-gadget.css', array(), '2.0.1' ); wp_enqueue_script( 'aistma-posts-gadget', AISTMA_URL . 'public/js/posts-gadget.js', array( 'jquery' ), '2.0.1', true ); wp_localize_script( 'aistma-posts-gadget', 'aistmaPostsGadget', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aistma_posts_gadget_nonce' ), 'strings' => array( 'loading' => __( 'Loading...', 'ai-story-maker' ), 'error' => __( 'An error occurred. Please try again.', 'ai-story-maker' ), 'no_posts' => __( 'No posts found.', 'ai-story-maker' ), 'search_placeholder' => __( 'Search posts...', 'ai-story-maker' ), 'filter_all' => __( 'All Posts', 'ai-story-maker' ), 'filter_new' => __( 'New Posts', 'ai-story-maker' ), 'filter_popular' => __( 'Popular', 'ai-story-maker' ), 'filter_recent' => __( 'Recent', 'ai-story-maker' ), ) ) ); } } /** * Render the posts gadget shortcode * * @param array $atts Shortcode attributes. * @return string HTML output. */ public function render_shortcode( $atts ) { $config = shortcode_atts( $this->default_config, $atts ); // Sanitize and validate configuration $config = $this->sanitize_config( $config ); // Generate unique ID for this gadget instance $gadget_id = 'aistma-posts-gadget-' . uniqid(); // Get initial posts $posts_data = $this->get_posts( $config ); // Start output buffering ob_start(); ?>
render_posts( $posts_data['posts'], $config ) ); ?>
'post', 'post_status' => 'publish', 'posts_per_page' => intval( $config['posts_per_page'] ), 'paged' => 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for posts gadget filtering functionality 'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Required for posts gadget taxonomy filtering 'tax_query' => array(), ); // Apply sorting $this->apply_sorting( $args, $config ); // Apply filters $this->apply_filters( $args, $config ); // Apply date range $this->apply_date_range( $args, $config ); $query = new \WP_Query( $args ); $posts = $query->posts; return array( 'posts' => $posts, 'has_more' => $query->max_num_pages > 1, 'total_pages' => $query->max_num_pages, 'found_posts' => $query->found_posts ); } /** * Apply sorting to query arguments * * @param array $args Query arguments. * @param array $config Configuration. */ private function apply_sorting( &$args, $config ) { switch ( $config['sort_by'] ) { case 'popular': $args['orderby'] = 'comment_count'; break; case 'random': $args['orderby'] = 'rand'; break; case 'title': $args['orderby'] = 'title'; break; default: // date $args['orderby'] = 'date'; break; } $args['order'] = strtoupper( $config['sort_order'] ); } /** * Apply filters to query arguments * * @param array $args Query arguments. * @param array $config Configuration. */ private function apply_filters( &$args, $config ) { // Ensure tax_query is initialized to an array to avoid undefined index warnings if ( ! isset( $args['tax_query'] ) || ! is_array( $args['tax_query'] ) ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Required for posts gadget category/tag filtering $args['tax_query'] = array(); } // Categories if ( ! empty( $config['categories'] ) ) { $categories = array_map( 'intval', explode( ',', $config['categories'] ) ); $args['tax_query'][] = array( 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => $categories, ); } // Tags if ( ! empty( $config['tags'] ) ) { $tags = array_map( 'intval', explode( ',', $config['tags'] ) ); $args['tax_query'][] = array( 'taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => $tags, ); } // Authors if ( ! empty( $config['authors'] ) ) { $authors = array_map( 'intval', explode( ',', $config['authors'] ) ); $args['author__in'] = $authors; } // Handle multiple tax queries if ( count( $args['tax_query'] ) > 1 ) { $args['tax_query']['relation'] = 'AND'; } } /** * Apply date range filter * * @param array $args Query arguments. * @param array $config Configuration. */ private function apply_date_range( &$args, $config ) { if ( empty( $config['date_range'] ) ) { return; } $date_query = array(); switch ( $config['date_range'] ) { case 'today': $date_query = array( 'after' => 'today', ); break; case 'week': $date_query = array( 'after' => '1 week ago', ); break; case 'month': $date_query = array( 'after' => '1 month ago', ); break; case 'year': $date_query = array( 'after' => '1 year ago', ); break; } if ( ! empty( $date_query ) ) { $args['date_query'] = array( $date_query ); } } /** * Render posts HTML * * @param array $posts Array of WP_Post objects. * @param array $config Configuration. * @return string HTML output. */ private function render_posts( $posts, $config ) { if ( empty( $posts ) ) { return '
' . esc_html( $this->get_localized_string( 'no_posts' ) ) . '
'; } $output = ''; $new_post_days = intval( $config['new_post_days'] ); foreach ( $posts as $post ) { $is_new = $config['highlight_new'] && $this->is_new_post( $post, $new_post_days ); $output .= $this->render_single_post( $post, $config, $is_new ); } return $output; } /** * Render single post HTML * * @param WP_Post $post Post object. * @param array $config Configuration. * @param bool $is_new Whether this is a new post. * @return string HTML output. */ private function render_single_post( $post, $config, $is_new = false ) { $post_classes = array( 'aistma-post-item' ); if ( $is_new ) { $post_classes[] = 'aistma-post-new'; } $output = '
'; // Featured image if ( $config['show_featured_image'] ) { $thumbnail = get_the_post_thumbnail( $post->ID, $config['image_size'], array( 'class' => 'aistma-post-thumbnail', 'loading' => 'lazy' ) ); if ( $thumbnail ) { $output .= '
'; $output .= ''; $output .= $thumbnail; $output .= ''; if ( $is_new ) { $output .= '' . __( 'New', 'ai-story-maker' ) . ''; } $output .= '
'; } } // Post content $output .= '
'; // Title $output .= '

'; $output .= ''; $output .= esc_html( get_the_title( $post->ID ) ); $output .= ''; $output .= '

'; // Excerpt if ( $config['show_excerpt'] ) { $excerpt = $this->get_post_excerpt( $post, $config['excerpt_length'] ); if ( $excerpt ) { $output .= '
'; $output .= wp_kses_post( $excerpt ); $output .= '
'; } } // Read more link if ( $config['show_read_more'] ) { $output .= ''; } $output .= '
'; $output .= '
'; return $output; } /** * Get post excerpt * * @param WP_Post $post Post object. * @param int $length Excerpt length. * @return string Post excerpt. */ private function get_post_excerpt( $post, $length ) { if ( ! empty( $post->post_excerpt ) ) { return wp_trim_words( $post->post_excerpt, $length, '...' ); } $content = strip_shortcodes( $post->post_content ); $content = wp_strip_all_tags( $content ); return wp_trim_words( $content, $length, '...' ); } /** * Check if post is new * * @param WP_Post $post Post object. * @param int $days Number of days to consider as "new". * @return bool Whether post is new. */ private function is_new_post( $post, $days ) { $post_date = strtotime( $post->post_date ); $cutoff_date = strtotime( "-{$days} days" ); return $post_date > $cutoff_date; } /** * AJAX handler for loading more posts */ public function ajax_load_posts() { check_ajax_referer( 'aistma_posts_gadget_nonce', 'nonce' ); $page = intval( $_POST['page'] ?? 1 ); $config_raw = isset( $_POST['config'] ) ? sanitize_text_field( wp_unslash( $_POST['config'] ) ) : '{}'; $config = json_decode( stripslashes( $config_raw ), true ); $config = array_merge( $this->default_config, $config ); $config['posts_per_page'] = intval( $config['posts_per_page'] ); $page = max( 1, $page ); $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => $config['posts_per_page'], 'paged' => $page, ); $this->apply_sorting( $args, $config ); $this->apply_filters( $args, $config ); $this->apply_date_range( $args, $config ); $query = new \WP_Query( $args ); $posts = $query->posts; $response = array( 'success' => true, 'posts' => $this->render_posts( $posts, $config ), 'has_more' => $query->max_num_pages > $page, 'next_page' => $page + 1, ); wp_send_json( $response ); } /** * AJAX handler for searching posts */ public function ajax_search_posts() { check_ajax_referer( 'aistma_posts_gadget_nonce', 'nonce' ); $search_term = sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) ); $config_raw = isset( $_POST['config'] ) ? sanitize_text_field( wp_unslash( $_POST['config'] ) ) : '{}'; $config = json_decode( stripslashes( $config_raw ), true ); $config = array_merge( $this->default_config, $config ); if ( empty( $search_term ) ) { $posts_data = $this->get_posts( $config ); } else { $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => intval( $config['posts_per_page'] ), 's' => $search_term, ); $this->apply_sorting( $args, $config ); $this->apply_filters( $args, $config ); $this->apply_date_range( $args, $config ); $query = new \WP_Query( $args ); $posts_data = array( 'posts' => $query->posts, 'has_more' => false, // No pagination for search results 'total_pages' => 1, 'found_posts' => $query->found_posts ); } $response = array( 'success' => true, 'posts' => $this->render_posts( $posts_data['posts'], $config ), 'has_more' => $posts_data['has_more'], ); wp_send_json( $response ); } /** * Sanitize configuration * * @param array $config Configuration array. * @return array Sanitized configuration. */ private function sanitize_config( $config ) { $sanitized = array(); foreach ( $this->default_config as $key => $default_value ) { if ( ! isset( $config[ $key ] ) ) { $sanitized[ $key ] = $default_value; continue; } switch ( $key ) { case 'posts_per_page': case 'excerpt_length': case 'new_post_days': $sanitized[ $key ] = max( 1, intval( $config[ $key ] ) ); break; case 'image_size': $allowed_sizes = array( 'thumbnail', 'medium', 'large', 'full' ); $sanitized[ $key ] = in_array( $config[ $key ], $allowed_sizes ) ? $config[ $key ] : $default_value; break; case 'sort_by': $allowed_sort = array( 'date', 'title', 'popular', 'random' ); $sanitized[ $key ] = in_array( $config[ $key ], $allowed_sort ) ? $config[ $key ] : $default_value; break; case 'sort_order': $sanitized[ $key ] = strtoupper( $config[ $key ] ) === 'ASC' ? 'ASC' : 'DESC'; break; case 'layout': $allowed_layouts = array( 'grid', 'list' ); $sanitized[ $key ] = in_array( $config[ $key ], $allowed_layouts ) ? $config[ $key ] : $default_value; break; case 'date_range': $allowed_ranges = array( '', 'today', 'week', 'month', 'year' ); $sanitized[ $key ] = in_array( $config[ $key ], $allowed_ranges ) ? $config[ $key ] : $default_value; break; case 'categories': case 'tags': case 'authors': $sanitized[ $key ] = sanitize_text_field( $config[ $key ] ); break; case 'read_more_text': case 'load_more_text': $sanitized[ $key ] = sanitize_text_field( $config[ $key ] ); break; default: $sanitized[ $key ] = (bool) $config[ $key ]; break; } } return $sanitized; } /** * Get localized string * * @param string $key String key. * @return string Localized string. */ private function get_localized_string( $key ) { $strings = array( 'loading' => __( 'Loading...', 'ai-story-maker' ), 'error' => __( 'An error occurred. Please try again.', 'ai-story-maker' ), 'no_posts' => __( 'No posts found.', 'ai-story-maker' ), 'search_placeholder' => __( 'Search posts...', 'ai-story-maker' ), 'filter_all' => __( 'All Posts', 'ai-story-maker' ), 'filter_new' => __( 'New Posts', 'ai-story-maker' ), 'filter_popular' => __( 'Popular', 'ai-story-maker' ), 'filter_recent' => __( 'Recent', 'ai-story-maker' ), ); return $strings[ $key ] ?? $key; } }