feat: implement WooCommerce AJAX product filtering

This commit is contained in:
XShop
2026-09-13 20:31:20 +03:30
parent f5c283af25
commit b78e332966
15 changed files with 1543 additions and 150 deletions
@@ -1,11 +1,10 @@
<?php
/**
* Shop / category archive — uses Woo hooks, XShop toolbar/grid/empty states.
* Shop / category archive — XShop filters + AJAX grid + no-JS fallback.
*
* Override is minimal: we keep Woo's before/after hooks but ensure XShop
* container, breadcrumbs, title, sorting, result count, pagination, and empty state
* are rendered with XShop components/tokens. For full logic see
* inc/woocommerce/setup.php hooks (xshop_wc_loop_start, toolbar).
* Integrates product-filters sidebar (desktop) + drawer (mobile) + toolbar.
* Server render uses Woo hooks + ProductQuery via pre_get_posts (inc/woocommerce/setup.php) for no-JS.
* JS enhances via REST /xshop/v1/products.
*
* WC version tested: 11.1.0
* Template version: 3.4.0
@@ -33,36 +32,72 @@ if (is_product_category() || is_product_tag() || is_tax('product_brand')) {
$title = $shopId ? get_the_title($shopId) : esc_html__('Shop', 'xshop');
echo '<header class="xshop-page-header"><h1 class="xshop-page-header__title">' . esc_html($title) . '</h1></header>';
}
?>
do_action('woocommerce_before_shop_loop');
<div class="xshop-shop" data-xshop-shop>
<!-- Mobile filter toggle -->
<button type="button" class="xshop-btn xshop-btn--secondary xshop-shop__filter-toggle" data-xshop-drawer-toggle aria-expanded="false" aria-controls="xshop-filter-drawer">
<?php esc_html_e('Filters', 'xshop'); ?>
</button>
if (woocommerce_product_loop()) {
woocommerce_product_loop_start();
if (wc_get_loop_prop('is_shortcode')) {
$columns = absint(wc_get_loop_prop('columns'));
} else {
$columns = absint(get_option('woocommerce_catalog_columns', 4));
}
<div class="xshop-shop__layout">
<!-- Desktop sidebar -->
<aside class="xshop-shop__sidebar" aria-label="<?php esc_attr_e('Filters', 'xshop'); ?>">
<?php get_template_part('template-parts/components/product-filters'); ?>
</aside>
while (have_posts()) {
the_post();
do_action('woocommerce_shop_loop');
wc_get_template_part('content', 'product');
}
<!-- Mobile drawer -->
<div class="xshop-drawer" id="xshop-filter-drawer" hidden data-xshop-drawer role="dialog" aria-modal="true" aria-label="<?php esc_attr_e('Filters', 'xshop'); ?>">
<div class="xshop-drawer__header">
<h2 class="xshop-drawer__title"><?php esc_html_e('Filters', 'xshop'); ?></h2>
<button type="button" class="xshop-btn xshop-btn--ghost" data-xshop-drawer-close aria-label="<?php esc_attr_e('Close filters', 'xshop'); ?>">×</button>
</div>
<div class="xshop-drawer__body">
<?php get_template_part('template-parts/components/product-filters'); ?>
</div>
</div>
<div class="xshop-drawer__overlay" hidden data-xshop-drawer-overlay></div>
woocommerce_product_loop_end();
do_action('woocommerce_after_shop_loop');
} else {
do_action('woocommerce_no_products_found');
// Enhanced empty state for shop/category
get_template_part('template-parts/components/empty-state', null, [
'title' => esc_html__('No products found', 'xshop'),
'text' => esc_html__('Try adjusting your filters or browse all products.', 'xshop'),
'cta_label' => esc_html__('Browse shop', 'xshop'),
'cta_url' => wc_get_page_permalink('shop'),
]);
}
<!-- Main -->
<div class="xshop-shop__main" data-xshop-shop-main>
<div class="xshop-shop__toolbar" data-xshop-toolbar>
<?php do_action('woocommerce_before_shop_loop'); ?>
</div>
<div class="xshop-search__loading" hidden data-xshop-loading style="display:none">
<span class="xshop-loading__spinner" aria-hidden="true"></span> <?php esc_html_e('Loading…', 'xshop'); ?>
</div>
<div class="xshop-search__error" hidden data-xshop-error role="alert"></div>
<div data-xshop-products-container>
<?php
if (woocommerce_product_loop()) {
woocommerce_product_loop_start();
while (have_posts()) { the_post(); do_action('woocommerce_shop_loop'); wc_get_template_part('content', 'product'); }
woocommerce_product_loop_end();
do_action('woocommerce_after_shop_loop');
} else {
do_action('woocommerce_no_products_found');
get_template_part('template-parts/components/empty-state', null, [
'title' => esc_html__('No products found', 'xshop'),
'text' => esc_html__('Try adjusting your filters or browse all products.', 'xshop'),
'cta_label' => esc_html__('Browse shop', 'xshop'),
'cta_url' => wc_get_page_permalink('shop'),
]);
}
?>
</div>
<div data-xshop-pagination>
<?php
// Woo pagination rendered via woocommerce_after_shop_loop already inside loop; for AJAX we render custom via JS
// Ensure result count is accessible
?>
</div>
</div>
</div>
</div>
<?php
do_action('woocommerce_after_main_content');
get_footer('shop');