// 1. Recursive Helper Function to Render Parent-Child Dropdown Options
if (!function_exists(‘render_cpl_dropdown_hierarchy’)) {
function render_cpl_dropdown_hierarchy($categories, $parent_id = 0, $depth = 0, $selected = ”) {
if (!is_array($categories) || empty($categories)) {
return;
}
foreach ($categories as $cat) {
if (isset($cat->parent) && (int)$cat->parent === (int)$parent_id) {
$indent = str_repeat(‘   ’, $depth);
$is_selected = (strcasecmp($cat->slug, $selected) === 0) ? ‘ selected’ : ”;
echo ‘<option value=”‘ . esc_attr($cat->slug) . ‘”‘ . $is_selected . ‘>’ . $indent . esc_html($cat->name) . ‘</option>’;
render_cpl_dropdown_hierarchy($categories, $cat->term_id, $depth + 1, $selected);
}
}
}
}

// 2. Shortcode Handler:

Showing 0 of 0 posts

add_shortcode(‘custom_post_loader’, ‘render_custom_post_loader_shortcode’);

function render_custom_post_loader_shortcode($atts) {
$atts = shortcode_atts([
‘post_types’ => ‘post’,
‘post_status’ => ‘publish’,
‘category_slugs’ => ”, // Pre-selected category slug (optional)
‘author’ => ”, // Author slug, username, or ID (empty = all authors)
‘taxonomy’ => ‘category’, // Custom taxonomy support (default: category)
‘posts_per_page’ => 12,
], $atts);

$post_types = sanitize_text_field($atts[‘post_types’]);
$post_status = sanitize_key($atts[‘post_status’]);
$category_slug = sanitize_text_field($atts[‘category_slugs’]);
$author_param = sanitize_text_field($atts[‘author’]);
$taxonomy = sanitize_key($atts[‘taxonomy’]);
$per_page = intval($atts[‘posts_per_page’]);

// Resolve author to numeric ID if provided
$author_id = 0;
if (!empty($author_param)) {
if (is_numeric($author_param)) {
$author_id = (int) $author_param;
} else {
$user_obj = get_user_by(‘slug’, sanitize_title($author_param));
if (!$user_obj) {
$user_obj = get_user_by(‘login’, sanitize_user($author_param));
}
if ($user_obj) {
$author_id = $user_obj->ID;
}
}
}

// Safely retrieve taxonomy terms
$tax_name = taxonomy_exists($taxonomy) ? $taxonomy : ‘category’;
$terms = get_terms([
‘taxonomy’ => $tax_name,
‘hide_empty’ => false,
]);

// Check if hierarchy exists (any terms have a parent)
$has_hierarchy = false;
if (!empty($terms) && !is_wp_error($terms)) {
foreach ($terms as $term) {
if (!empty($term->parent)) {
$has_hierarchy = true;
break;
}
}
}

ob_start(); ?>
<style>
.cpl-container {
margin: 20px 0;
font-family: inherit;
}
.cpl-filter-wrapper {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.cpl-select {
width: 100%;
max-width: 320px;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
outline: none;
background: #fff;
}
.custom-posts-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.custom-post-item {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
background: #fff;
display: flex;
flex-direction: column;
}
.custom-post-item img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.custom-post-item-content {
padding: 15px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.custom-post-item-content .post-meta {
font-size: 12px;
color: #64748b;
margin-bottom: 8px;
}
.custom-post-item-content .blog-title {
margin: 0 0 10px;
font-size: 18px;
line-height: 1.4;
}
.custom-post-item-content .blog-title a {
text-decoration: none;
color: #1e293b;
}
.cpl-footer {
margin-top: 30px;
text-align: center;
}
#post-count-display {
font-size: 14px;
color: #475569;
margin-bottom: 12px;
}
#load-more-btn {
background-color: #0f172a;
color: #ffffff;
border: none;
padding: 10px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
}
#load-more-btn:hover {
background-color: #334155;
}
#loading-spinner {
font-size: 14px;
color: #64748b;
margin-top: 10px;
}
</style>

<div class=”cpl-container”>
<!– Category Filter –>
<div class=”cpl-filter-wrapper”>
<select id=”category-filter” class=”cpl-select”>
<option value=””>All Categories</option>
<?php if (!empty($terms) && !is_wp_error($terms)) : ?>
<?php if ($has_hierarchy) : ?>
<?php render_cpl_dropdown_hierarchy($terms, 0, 0, $category_slug); ?>
<?php else : ?>
<?php foreach ($terms as $t) : ?>
<option value=”<?php echo esc_attr($t->slug); ?>” <?php selected(strtolower($category_slug), strtolower($t->slug)); ?>>
<?php echo esc_html($t->name); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</select>
</div>

<!– Post Grid –>
<div id=”custom-posts” class=”custom-posts-container”></div>

<!– Pagination & Status –>
<div class=”cpl-footer”>
<div id=”post-count-display”>
Showing <span id=”shown-posts”>0</span> of <span id=”total-posts”>0</span> posts
</div>
<div id=”load-more”>
<button id=”load-more-btn” style=”display:none;”>Load More</button>
</div>
<div id=”loading-spinner” style=”display:none;”>Loading…</div>
</div>
</div>

<script>
document.addEventListener(‘DOMContentLoaded’, function() {
var page = 1;
var selectedCategory = “<?php echo esc_js($category_slug); ?>”;
var container = document.getElementById(‘custom-posts’);
var btn = document.getElementById(‘load-more-btn’);
var shown = document.getElementById(‘shown-posts’);
var total = document.getElementById(‘total-posts’);
var spinner = document.getElementById(‘loading-spinner’);
var filter = document.getElementById(‘category-filter’);

var postTypes = “<?php echo esc_js($post_types); ?>”;
var authorId = “<?php echo esc_js($author_id); ?>”;
var taxonomy = “<?php echo esc_js($tax_name); ?>”;
var perPage = “<?php echo esc_js($per_page); ?>”;
var ajaxUrl = “<?php echo esc_url(admin_url(‘admin-ajax.php’)); ?>”;
var nonce = “<?php echo wp_create_nonce(‘cpl_nonce_action’); ?>”;

function fetchPosts(reset) {
if (reset) {
page = 1;
container.innerHTML = ”;
}

spinner.style.display = ‘block’;
btn.style.display = ‘none’;

var url = ajaxUrl + ‘?action=cpl_ajax_fetch’ +
‘&page=’ + page +
‘&nonce=’ + nonce +
‘&post_types=’ + encodeURIComponent(postTypes) +
‘&author=’ + encodeURIComponent(authorId) +
‘&taxonomy=’ + encodeURIComponent(taxonomy) +
‘&posts_per_page=’ + encodeURIComponent(perPage) +
‘&category_slug=’ + encodeURIComponent(selectedCategory);

fetch(url)
.then(function(res) { return res.json(); })
.then(function(data) {
spinner.style.display = ‘none’;
if (data && data.success && data.data) {
if (data.data.html) {
container.insertAdjacentHTML(‘beforeend’, data.data.html);
}
var count = container.children.length;
shown.textContent = count;
total.textContent = data.data.total;
btn.style.display = count < data.data.total ? ‘inline-block’ : ‘none’;
}
})
.catch(function(err) {
console.error(‘Fetch error:’, err);
spinner.style.display = ‘none’;
});
}

btn.addEventListener(‘click’, function() {
page++;
fetchPosts(false);
});

filter.addEventListener(‘change’, function() {
selectedCategory = filter.value;
fetchPosts(true);
});

if (selectedCategory) {
filter.value = selectedCategory;
}

fetchPosts(false);
});
</script>
<?php
return ob_get_clean();
}

// 3. AJAX Fetch Callback (Runs on dynamic button click and category change)
add_action(‘wp_ajax_cpl_ajax_fetch’, ‘cpl_ajax_fetch_callback’);
add_action(‘wp_ajax_nopriv_cpl_ajax_fetch’, ‘cpl_ajax_fetch_callback’);

function cpl_ajax_fetch_callback() {
check_ajax_referer(‘cpl_nonce_action’, ‘nonce’);

$paged = max(1, intval($_GET[‘page’] ?? 1));
$post_types = array_map(‘sanitize_text_field’, explode(‘,’, $_GET[‘post_types’] ?? ‘post’));
$category_slug = sanitize_text_field($_GET[‘category_slug’] ?? ”);
$author_id = intval($_GET[‘author’] ?? 0);
$taxonomy = sanitize_key($_GET[‘taxonomy’] ?? ‘category’);
$posts_per_page = intval($_GET[‘posts_per_page’] ?? 12);

$args = [
‘post_type’ => $post_types,
‘post_status’ => ‘publish’,
‘posts_per_page’ => $posts_per_page,
‘paged’ => $paged,
];

if ($author_id > 0) {
$args[‘author’] = $author_id;
}

if (!empty($category_slug)) {
$target_tax = taxonomy_exists($taxonomy) ? $taxonomy : ‘category’;
$args[‘tax_query’] = [
[
‘taxonomy’ => $target_tax,
‘field’ => ‘slug’,
‘terms’ => $category_slug,
‘include_children’ => true,
],
];
}

$q = new WP_Query($args);
$html = ”;

if ($q->have_posts()) {
while ($q->have_posts()) {
$q->the_post();
$permalink = esc_url(get_permalink());
$title = esc_html(get_the_title());
$date = esc_html(get_the_date());
$author_name = esc_html(get_the_author());

$html .= “<div class=’custom-post-item’>”;
if (has_post_thumbnail()) {
$html .= “<a href='{$permalink}’>” . get_the_post_thumbnail(get_the_ID(), ‘medium’, [‘alt’ => $title]) . “</a>”;
} else {
$fallback = esc_url(site_url(‘/wp-content/uploads/2025/10/DefaultImage.jpg’));
$html .= “<a href='{$permalink}’><img src='{$fallback}’ alt='{$title}’ /></a>”;
}
$html .= “<div class=’custom-post-item-content’>”;
$html .= “<p class=’post-meta’><span class=’post-date’>{$date}</span> | <span class=’post-author’>By {$author_name}</span></p>”;
$html .= “<h3 class=’blog-title’><a href='{$permalink}’>{$title}</a></h3>”;
$html .= “</div></div>”;
}
wp_reset_postdata();
}

wp_send_json_success([
‘html’ => $html,
‘total’ => intval($q->found_posts),
]);
}