Custom Product Tabs AJAX

User
<script>
document.addEventListener('DOMContentLoaded', function () {
        const tabs = document.querySelectorAll('.tab-button');
        const contents = document.querySelectorAll('.tab-content');
        tabs.forEach(tab => {
            tab.addEventListener('click', function () {
                tabs.forEach(t => t.classList.remove('active'));
                contents.forEach(c => c.style.display = 'none');
                this.classList.add('active');
                document.getElementById('tab-' + this.dataset.tab).style.display = 'block';
            });
        });
    });
</script>

don't use short code just user custom code and ajax and apply owl and display 4 items one time

Assistant
Here's a complete working example using **custom WP_Query**, **AJAX**, and **Owl Carousel** to replace `[products]` shortcodes with dynamic product loading via tabs (`new`, `sales`, `bestsellers`, `spotlights`).

### ✅ PHP Part – Shortcode + AJAX Handler

Add this to your theme’s `functions.php` or a custom plugin:

add_shortcode('custom_product_tabs_slider', 'custom_product_tabs_slider_func');
function custom_product_tabs_slider_func() {
    ob_start();
    ?>
    <div class="product-tabs-wrapper">
        <div class="product-tabs-nav">
            <button class="tab-button active" data-tab="new">New</button>
            <button class="tab-button" data-tab="sales">Sales</button>
            <button class="tab-button" data-tab="bestsellers">Bestsellers</button>
            <button class="tab-button" data-tab="spotlights">Spotlights</button>
        </div>

        <div class="tab-content-wrapper">
            <div class="owl-carousel owl-theme" id="product-tab-slider"></div>
        </div>
    </div>
    <script>
    document.addEventListener('DOMContentLoaded', function () {
        const tabs = document.querySelectorAll('.tab-button');
        const content = document.querySelector('#product-tab-slider');

        function fetchProducts(tab) {
            content.innerHTML = '<div class="loading">Loading...</div>';
            fetch('<?php echo admin_url('admin-ajax.php'); ?>?action=load_products_tab&type=' + tab)
                .then(res => res.text())
                .then(html => {
                    content.innerHTML = html;
                    jQuery('#product-tab-slider').owlCarousel({
                        items: 4,
                        loop: false,
                        margin: 10,
                        nav: true,
                        dots: false,
                        responsive: {
                            0: { items: 1 },
                            600: { items: 2 },
                            1000: { items: 4 }
                        }
                    });
                });
        }

        tabs.forEach(tab => {
            tab.addEventListener('click', function () {
                tabs.forEach(t => t.classList.remove('active'));
                this.classList.add('active');
                jQuery('#product-tab-slider').trigger('destroy.owl.carousel');
                fetchProducts(this.dataset.tab);
            });
        });

        // Load initial tab
        fetchProducts('new');
    });
    </script>
    <?php
    return ob_get_clean();
}

// AJAX handler
add_action('wp_ajax_load_products_tab', 'ajax_load_products_tab');
add_action('wp_ajax_nopriv_load_products_tab', 'ajax_load_products_tab');

function ajax_load_products_tab() {
    $type = $_GET['type'] ?? 'new';

    $args = [
        'post_type' => 'product',
        'posts_per_page' => 8,
        'post_status' => 'publish'
    ];

    switch ($type) {
        case 'sales':
            $args['meta_query'][] = [
                'key' => '_sale_price',
                'value' => 0,
                'compare' => '>',
                'type' => 'NUMERIC'
            ];
            break;
        case 'bestsellers':
            $args['orderby'] = 'meta_value_num';
            $args['meta_key'] = 'total_sales';
            break;
        case 'spotlights':
            $args['tax_query'][] = [
                'taxonomy' => 'product_visibility',
                'field' => 'name',
                'terms' => 'featured',
                'operator' => 'IN'
            ];
            break;
        case 'new':
        default:
            $args['orderby'] = 'date';
            $args['order'] = 'DESC';
            break;
    }

    $query = new WP_Query($args);
    if ($query->have_posts()) :
        while ($query->have_posts()): $query->the_post();
            global $product;
            ?>
            <div class="item">
                <a href="<?php the_permalink(); ?>" class="product-card">
                    <?php echo woocommerce_get_product_thumbnail(); ?>
                    <h3><?php the_title(); ?></h3>
                    <span class="price"><?php echo $product->get_price_html(); ?></span>
                </a>
            </div>
            <?php
        endwhile;
        wp_reset_postdata();
    else:
        echo '<p>No products found.</p>';
    endif;

    wp_die();
}

### ✅ Enqueue Owl Carousel Scripts

Add this to `functions.php`:

function enqueue_owl_assets() {
    wp_enqueue_style('owl-carousel', 'https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css');
    wp_enqueue_style('owl-theme', 'https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.theme.default.min.css');
    wp_enqueue_script('owl-carousel', 'https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js', ['jquery'], null, true);
}
add_action('wp_enqueue_scripts', 'enqueue_owl_assets');

### ✅ Use in Page Builder or Template

echo do_shortcode('[custom_product_tabs_slider]');

Let me know if you want autoplay or additional carousel settings.

User
on_sale="true" wp query
Assistant
To fetch WooCommerce products that are **on sale** using `WP_Query`, you need to query products that have a `_sale_price` meta key with a value greater than 0.

Here’s the correct `WP_Query` arguments for `on_sale="true"` equivalent:

$args = [
    'post_type'      => 'product',
    'posts_per_page' => 8,
    'post_status'    => 'publish',
    'meta_query'     => [
        [
            'key'     => '_sale_price',
            'value'   => 0,
            'compare' => '>',
            'type'    => 'NUMERIC',
        ]
    ]
];

### ✅ Optional: Combine with stock status
To exclude out-of-stock products, you can enhance the `meta_query`:

'meta_query' => [
    'relation' => 'AND',
    [
        'key'     => '_sale_price',
        'value'   => 0,
        'compare' => '>',
        'type'    => 'NUMERIC',
    ],
    [
        'key'     => '_stock_status',
        'value'   => 'instock'
    ]
]

### ✅ Optional: Add taxonomy filter if needed
To query only from specific categories:

'tax_query' => [
    [
        'taxonomy' => 'product_cat',
        'field'    => 'slug',
        'terms'    => ['your-category-slug'],
    ]
]

Let me know if you want the complete integration in your tab-ajax slider.

User
<script>
document.addEventListener('DOMContentLoaded', function () {
        const tabs = document.querySelectorAll('.tab-button');
        const content = document.querySelector('#product-tab-slider');

        function fetchProducts(tab) {
            content.innerHTML = '<div class="loader"></div>';
            fetch('<?php echo admin_url('admin-ajax.php'); ?>?action=load_products_tab&type=' + tab)
                .then(res => res.text())
                .then(html => {
                    content.innerHTML = html;
                    jQuery('#product-tab-slider').owlCarousel({
                        items: 4,
                        loop: false,
                        margin: 10,
                        nav: true,
                        dots: false,
                        responsive: {
                            0: { items: 1 },
                            600: { items: 2 },
                            1000: { items: 4 }
                        }
                    });
                });
        }

        tabs.forEach(tab => {
            tab.addEventListener('click', function () {
                tabs.forEach(t => t.classList.remove('active'));
                this.classList.add('active');
                jQuery('#product-tab-slider').trigger('destroy.owl.carousel');
                fetchProducts(this.dataset.tab);
            });
        });

        // Load initial tab
        fetchProducts('new');
    });
</script>

add loader and apply smooth tab click result show

Assistant
Here’s your **updated code** with:

– ✅ A smooth fade-in effect on tab content load
– ✅ A proper **CSS spinner loader** while fetching
– ✅ Improved UX on tab click

### ✅ HTML + JS (Tab Markup with AJAX + Smooth UI)

<div class="product-tabs-wrapper">
    <div class="product-tabs-nav">
        <button class="tab-button active" data-tab="new"><?php echo __('New','flatsome')?></button>
        <button class="tab-button" data-tab="sales"><?php echo __('Sales','flatsome')?></button>
        <button class="tab-button" data-tab="bestsellers"><?php echo __('Bestsellers','flatsome')?></button>
        <button class="tab-button" data-tab="spotlights"><?php echo __('Spotlights','flatsome')?></button>
    </div>

    <div class="tab-content-wrapper">
        <div class="loader" style="display: none;"></div>
        <div class="owl-carousel owl-theme" id="product-tab-slider"></div>
    </div>
</div>

<style>
.product-tabs-wrapper {
    margin-top: 20px;
}
.product-tabs-nav {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
}
.tab-button {
    padding: 10px 20px;
    border: 1px solid #ccc;
    background: #f2f2f2;
    cursor: pointer;
    border-radius: 4px;
    transition: background 0.3s;
}
.tab-button.active {
    background: #19bdc0;
    color: #fff;
    border-color: #19bdc0;
}
.loader {
    border: 5px solid #f3f3f3;
    border-top: 5px solid #19bdc0;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 0.8s linear infinite;
    margin: 40px auto;
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
.owl-carousel .item {
    opacity: 0;
    transform: translateY(10px);
    transition: all 0.3s ease-in-out;
}
.owl-carousel .item.loaded {
    opacity: 1;
    transform: translateY(0);
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function () {
    const tabs = document.querySelectorAll('.tab-button');
    const content = document.querySelector('#product-tab-slider');
    const loader = document.querySelector('.loader');

    function fetchProducts(tab) {
        content.innerHTML = '';
        loader.style.display = 'block';

        fetch('<?php echo admin_url('admin-ajax.php'); ?>?action=load_products_tab&type=' + tab)
            .then(res => res.text())
            .then(html => {
                loader.style.display = 'none';
                content.innerHTML = html;
                const items = content.querySelectorAll('.item');
                items.forEach((el, i) => {
                    setTimeout(() => el.classList.add('loaded'), i * 100);
                });

                jQuery('#product-tab-slider').owlCarousel({
                    items: 4,
                    loop: false,
                    margin: 10,
                    nav: true,
                    dots: false,
                    responsive: {
                        0: { items: 1 },
                        600: { items: 2 },
                        1000: { items: 4 }
                    }
                });
            });
    }

    tabs.forEach(tab => {
        tab.addEventListener('click', function () {
            tabs.forEach(t => t.classList.remove('active'));
            this.classList.add('active');
            jQuery('#product-tab-slider').trigger('destroy.owl.carousel');
            fetchProducts(this.dataset.tab);
        });
    });

    // Load initial tab
    fetchProducts('new');
});
</script>

### ✅ AJAX Output HTML Format (in PHP handler)

Make sure your `load_products_tab` handler returns items like:

echo '<div class="item"><a href="' . get_permalink() . '" class="product-card">';
echo woocommerce_get_product_thumbnail();
echo '<h3>' . get_the_title() . '</h3>';
echo '<span class="price">' . $product->get_price_html() . '</span>';
echo '</a></div>';

Add the class `.item` so each slide fades in smoothly with `.loaded`.

Let me know if you want **autoplay**, **lazy load**, or **custom layout per tab**.

User
display default image if image no available
Assistant
To display a **default image** if the product has no featured image, modify your code like this:

### ✅ PHP Code: Show default image if none is set

<?php
if (has_post_thumbnail()) {
    echo woocommerce_get_product_thumbnail();
} else {
    echo '<img src="' . esc_url(get_template_directory_uri() . '/assets/images/default-product.jpg') . '" alt="Default Product" />';
}
?>

### 📝 Notes:

– Replace `/assets/images/default-product.jpg` with the actual path to your fallback image.
– You can also use `get_stylesheet_directory_uri()` if the image is in a **child theme**.

### ✅ Optional: Global fallback image in `functions.php`

If you want to set a fallback image globally across WooCommerce:

add_filter('woocommerce_placeholder_img_src', 'custom_woocommerce_placeholder_img_src');
function custom_woocommerce_placeholder_img_src($src) {
    return get_template_directory_uri() . '/assets/images/default-product.jpg';
}

This automatically replaces the default WooCommerce placeholder.

Let me know if you want to apply this fallback for product loop thumbnails or specific WooCommerce hooks.

User
just add under line under current tab
Assistant
To add an **underline under the active tab** in your `.product-tabs-nav`, you can do it with a simple CSS rule targeting the `.active` tab.

### ✅ CSS Update – Underline for Active Tab

Add this inside your “ block:

.tab-button.active {
    position: relative;
}

.tab-button.active::after {
    content: "";
    position: absolute;
    bottom: -5px;
    left: 0;
    height: 3px;
    width: 100%;
    background-color: #19bdc0; /* Adjust to your brand color */
    border-radius: 2px;
}

### ✅ Result

– Adds a **3px thick colored underline** directly under the active tab.
– Positioned smoothly with some space (`bottom: -5px`) so it doesn’t touch text.

Let me know if you want a smooth transition effect or animation too.

User
Assistant
If the **image is set in the database (postmeta `_thumbnail_id`) but the actual file is missing on the server**, `has_post_thumbnail()` will still return `true`, but the image will not render correctly.

To handle this, you need to **check if the image file exists** in the filesystem, not just if the thumbnail ID is set.

### ✅ Full Code: Fallback if file missing

<?php
$thumbnail_id = get_post_thumbnail_id();
$image_src = wp_get_attachment_image_src($thumbnail_id, 'medium');

if ($image_src && file_exists(get_attached_file($thumbnail_id))) {
    echo '<img src="' . esc_url($image_src[0]) . '" alt="' . esc_attr(get_the_title()) . '">';
} else {
    echo '<img src="' . esc_url(DEFAULT_IMG) . '" alt="' . esc_attr(get_the_title()) . '">';
}
?>

### ✅ Notes:

– `get_attached_file($thumbnail_id)` returns the **full path** to the image file on the server.
– `file_exists()` ensures the file **actually exists**, not just in the DB.
– `DEFAULT_IMG` should be a full URL like:

define('DEFAULT_IMG', get_template_directory_uri() . '/assets/images/default-product.jpg');

Let me know if you also want to log broken images or replace missing ones in bulk.

User
Not working
Assistant
If the image exists in the database (i.e. a featured image is set), but the file is missing from the `uploads` directory, then:

– `has_post_thumbnail()` returns `true`
– `wp_get_attachment_image_src()` gives a URL
– BUT the file isn't physically there, so the image is broken

### ✅ Use this **robust fallback** code:

<?php
$thumbnail_id = get_post_thumbnail_id();

if ($thumbnail_id) {
    $image_path = get_attached_file($thumbnail_id); // Absolute path on disk
    $image_src = wp_get_attachment_image_src($thumbnail_id, 'medium'); // Get URL

    if ($image_src && file_exists($image_path)) {
        echo '<img src="' . esc_url($image_src[0]) . '" alt="' . esc_attr(get_the_title()) . '">';
    } else {
        echo '<img src="' . esc_url(get_template_directory_uri() . '/assets/images/default-product.jpg') . '" alt="' . esc_attr(get_the_title()) . '">';
    }
} else {
    echo '<img src="' . esc_url(get_template_directory_uri() . '/assets/images/default-product.jpg') . '" alt="' . esc_attr(get_the_title()) . '">';
}
?>

### ✅ Checklist:

1. Make sure this path is correct and the file actually exists:

your-theme/assets/images/default-product.jpg

2. If using a **child theme**, change:

get_template_directory_uri()

to:

get_stylesheet_directory_uri()

### 💡 Tip:
To test this, temporarily rename a file in `/uploads` that’s linked to a product’s featured image and reload.

Let me know if you want to **auto-replace broken image links in the database**, or show a WooCommerce product fallback image globally.

Leave a Comment

Your email address will not be published. Required fields are marked *