'; echo '
'; /* Preserve any taxonomy archive context: when on a category * archive, WP needs the term param to stay on the same URL. */ $qo = get_queried_object(); if ( $qo instanceof WP_Term && AumNexCart_Post_Type::TAXONOMY === $qo->taxonomy ) { echo ''; } if ( $enable_search ) { $this->render_search_filter(); } if ( $enable_cat ) { $this->render_category_filter(); } if ( $has_attrs ) { $this->render_attribute_filters( $attrs ); } if ( $enable_price ) { $this->render_price_filter(); } if ( $enable_sort ) { $this->render_sort_filter(); } echo '
'; echo ''; echo '' . esc_html__( 'Reset', 'aumnexcart' ) . ''; echo '
'; echo '
'; echo ''; } /** * Renders the category dropdown. Skipped if no terms exist. */ private function render_category_filter() { $terms = get_terms( array( 'taxonomy' => AumNexCart_Post_Type::TAXONOMY, 'hide_empty' => false, ) ); if ( is_wp_error( $terms ) || empty( $terms ) ) { return; } $current = isset( $_GET[ AumNexCart_Post_Type::TAXONOMY ] ) ? sanitize_text_field( wp_unslash( $_GET[ AumNexCart_Post_Type::TAXONOMY ] ) ) : ''; echo '
'; echo ''; echo ''; echo '
'; } /** * Returns the depth of a hierarchical term (0 for top-level). * * @param WP_Term $term * @return int */ private function term_depth( $term ) { $depth = 0; $current = $term; while ( $current && $current->parent ) { $depth++; $current = get_term( $current->parent, $term->taxonomy ); if ( is_wp_error( $current ) || ! $current ) { break; } if ( $depth > 10 ) { break; } } return $depth; } /** * Renders one dropdown per filterable attribute key. Values are * collected from shadow meta (fast — only rows with these keys * exist). * * @param array $keys */ private function render_attribute_filters( $keys ) { foreach ( $keys as $key ) { $slug = AumNexCart_Meta::attribute_key_to_slug( $key ); if ( '' === $slug ) { continue; } $values = $this->collect_attribute_values( $slug ); if ( empty( $values ) ) { continue; } $param = 'attr_' . $slug; $current = isset( $_GET[ $param ] ) ? sanitize_text_field( wp_unslash( $_GET[ $param ] ) ) : ''; echo '
'; echo ''; echo ''; echo '
'; } } /** * Renders the optional min/max price range inputs. */ private function render_price_filter() { $min = isset( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : ''; $max = isset( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : ''; echo '
'; echo ''; echo '
'; echo ''; echo '–'; echo ''; echo '
'; echo '
'; } /** * Renders the keyword search box. Uses a custom param (aumnexcart_s) rather * than the reserved `s` so WordPress keeps treating the request as a * product archive instead of switching to a site-wide search page; * the keyword is injected into the query as `s` server-side. */ private function render_search_filter() { $current = isset( $_GET['aumnexcart_s'] ) ? sanitize_text_field( wp_unslash( $_GET['aumnexcart_s'] ) ) : ''; echo ''; } /** * Renders the sort dropdown. Options come from get_sort_options() * so the front-end control and the query builders stay in sync. */ private function render_sort_filter() { $current = isset( $_GET['aumnexcart_orderby'] ) ? sanitize_key( wp_unslash( $_GET['aumnexcart_orderby'] ) ) : ''; echo '
'; echo ''; echo ''; echo '
'; } /** * The canonical sort option list (value => label). The first entry * is the implicit default (newest first). * * @return array */ public static function get_sort_options() { return array( 'date_desc' => __( 'Newest', 'aumnexcart' ), 'date_asc' => __( 'Oldest', 'aumnexcart' ), 'title_asc' => __( 'Name: A–Z', 'aumnexcart' ), 'title_desc' => __( 'Name: Z–A', 'aumnexcart' ), 'price_asc' => __( 'Price: low to high', 'aumnexcart' ), 'price_desc' => __( 'Price: high to low', 'aumnexcart' ), ); } /** * Resolves a sort key into query directives. Returns null for an * unknown/default key (caller leaves the query's natural order). * Price uses a sentinel 'price' orderby the caller expands into a * meta clause so unpriced products are kept, not filtered out. * * @param string $key * @return array|null { orderby, order } */ public static function resolve_sort( $key ) { $map = array( 'date_asc' => array( 'orderby' => 'date', 'order' => 'ASC' ), 'title_asc' => array( 'orderby' => 'title', 'order' => 'ASC' ), 'title_desc' => array( 'orderby' => 'title', 'order' => 'DESC' ), 'price_asc' => array( 'orderby' => 'price', 'order' => 'ASC' ), 'price_desc' => array( 'orderby' => 'price', 'order' => 'DESC' ), ); return isset( $map[ $key ] ) ? $map[ $key ] : null; } /** * Applies search + sort onto a WP_Query args array, returning the * modified array. Shared by the AJAX handler and the URL-mode * pre_get_posts adjuster so both paths behave identically. * * @param array $args WP_Query args (may already carry meta_query). * @param string $search Raw keyword ('' to skip). * @param string $sort_key Sort option key ('' for default). * @return array */ public static function apply_search_sort( array $args, $search, $sort_key ) { if ( '' !== $search ) { $args['s'] = $search; } $sort = self::resolve_sort( $sort_key ); if ( ! $sort ) { return $args; } if ( 'price' === $sort['orderby'] ) { $meta_query = isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : array(); /* OR clause keeps products with no price meta (text/hidden * types) in the result; they just sort to the end. The named * leaf 'aumnexcart_price_lookup' is what orderby references. */ $meta_query[] = array( 'relation' => 'OR', 'aumnexcart_price_lookup' => array( 'key' => AumNexCart_Meta::KEY_PRICE_MIN, 'type' => 'NUMERIC', 'compare' => 'EXISTS', ), array( 'key' => AumNexCart_Meta::KEY_PRICE_MIN, 'compare' => 'NOT EXISTS', ), ); $args['meta_query'] = $meta_query; $args['orderby'] = array( 'aumnexcart_price_lookup' => $sort['order'], 'date' => 'DESC', ); } else { $args['orderby'] = $sort['orderby']; $args['order'] = $sort['order']; } return $args; } /** * Builds a reset URL that strips all filter parameters but * keeps the current page context (taxonomy archive vs main * post-type archive). * * @return string */ private function get_reset_url() { $qo = get_queried_object(); if ( $qo instanceof WP_Term && AumNexCart_Post_Type::TAXONOMY === $qo->taxonomy ) { return get_term_link( $qo ); } return get_post_type_archive_link( AumNexCart_Post_Type::POST_TYPE ); } /* ---------------------------------------------------------------- * Attribute value collection * ---------------------------------------------------------------- */ /** * Returns the distinct values stored under a shadow meta key, * cached in a transient. The cache is invalidated whenever any * product is saved. * * @param string $slug * @return array */ public function collect_attribute_values( $slug ) { $cache_key = 'aumnexcart_attr_values_' . $slug; $cached = get_transient( $cache_key ); if ( false !== $cached && is_array( $cached ) ) { return $cached; } global $wpdb; $meta_key = AumNexCart_Meta::KEY_ATTR_SHADOW_PREFIX . $slug; $values = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != '' ORDER BY meta_value ASC", $meta_key ) ); $values = array_values( array_filter( array_map( 'strval', (array) $values ) ) ); set_transient( $cache_key, $values, HOUR_IN_SECONDS ); return $values; } /** * Static helper that clears all attribute value caches. Called * from save_post and from settings save when the filterable * list changes. */ public static function invalidate_value_cache() { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_aumnexcart_attr_values_' ) . '%', $wpdb->esc_like( '_transient_timeout_aumnexcart_attr_values_' ) . '%' ) ); } /* ---------------------------------------------------------------- * AJAX handler * ---------------------------------------------------------------- */ /** * Handles ?action=aumnexcart_filter requests. Builds the same * WP_Query the archive page would build and returns rendered * grid + pagination HTML fragments. */ public function ajax_handler() { check_ajax_referer( self::AJAX_ACTION, 'nonce' ); $args = array( 'post_type' => AumNexCart_Post_Type::POST_TYPE, 'posts_per_page' => (int) AumNexCart_Settings::get( 'products_per_page', 12 ), 'paged' => isset( $_POST['paged'] ) ? max( 1, absint( $_POST['paged'] ) ) : 1, ); /* Category */ $cat_slug = isset( $_POST[ AumNexCart_Post_Type::TAXONOMY ] ) ? sanitize_text_field( wp_unslash( $_POST[ AumNexCart_Post_Type::TAXONOMY ] ) ) : ''; if ( '' !== $cat_slug ) { $args['tax_query'] = array( array( 'taxonomy' => AumNexCart_Post_Type::TAXONOMY, 'field' => 'slug', 'terms' => $cat_slug, ), ); } /* Attributes via shadow meta */ $meta_query = array(); $filterable = AumNexCart_Settings::get( 'filterable_attributes', array() ); if ( is_array( $filterable ) ) { foreach ( $filterable as $key ) { $slug = AumNexCart_Meta::attribute_key_to_slug( $key ); if ( '' === $slug ) { continue; } $param = 'attr_' . $slug; if ( ! empty( $_POST[ $param ] ) ) { $value = sanitize_text_field( wp_unslash( $_POST[ $param ] ) ); $meta_query[] = array( 'key' => AumNexCart_Meta::KEY_ATTR_SHADOW_PREFIX . $slug, 'value' => $value, 'compare' => '=', ); } } } /* Price range */ if ( AumNexCart_Settings::get( 'enable_price_filter', 0 ) ) { if ( ! empty( $_POST['min_price'] ) ) { $meta_query[] = array( 'key' => AumNexCart_Meta::KEY_PRICE_MIN, 'value' => (float) sanitize_text_field( wp_unslash( $_POST['min_price'] ) ), 'type' => 'NUMERIC', 'compare' => '>=', ); } if ( ! empty( $_POST['max_price'] ) ) { $meta_query[] = array( 'key' => AumNexCart_Meta::KEY_PRICE_MAX, 'value' => (float) sanitize_text_field( wp_unslash( $_POST['max_price'] ) ), 'type' => 'NUMERIC', 'compare' => '<=', ); } } if ( ! empty( $meta_query ) ) { $args['meta_query'] = $meta_query; } $search = isset( $_POST['aumnexcart_s'] ) ? sanitize_text_field( wp_unslash( $_POST['aumnexcart_s'] ) ) : ''; $sort_key = isset( $_POST['aumnexcart_orderby'] ) ? sanitize_key( wp_unslash( $_POST['aumnexcart_orderby'] ) ) : ''; $args = self::apply_search_sort( $args, $search, $sort_key ); $q = new WP_Query( $args ); $grid_html = $this->render_grid( $q ); $pag_html = $this->render_pagination( $q ); wp_send_json_success( array( 'grid' => $grid_html, 'pagination' => $pag_html, 'found' => (int) $q->found_posts, ) ); } /** * Renders the grid HTML for a query, reusing the same markup as * the archive template so visual styling is identical. * * @param WP_Query $q * @return string */ private function render_grid( $q ) { ob_start(); if ( $q->have_posts() ) { echo '
'; while ( $q->have_posts() ) { $q->the_post(); $this->render_card(); } echo '
'; wp_reset_postdata(); } else { echo '

' . esc_html__( 'No products found.', 'aumnexcart' ) . '

'; } return ob_get_clean(); } /** * Renders one product card. Mirrors the markup in the archive * template; keep in sync if either changes. */ private function render_card() { $post_id = get_the_ID(); $show_cat = (int) AumNexCart_Settings::get( 'show_category_badge', 1 ); ?>
>
'lazy' ) ); ?>
' . esc_html( $first->name ) . ''; } } ?>

max_num_pages; if ( $total < 2 ) { return ''; } $current = max( 1, (int) $q->get( 'paged' ) ); $base = '%_%'; $format = '?paged=%#%'; $html = paginate_links( array( 'base' => $base, 'format' => $format, 'current' => $current, 'total' => $total, 'mid_size' => 2, 'prev_text' => __( 'Previous', 'aumnexcart' ), 'next_text' => __( 'Next', 'aumnexcart' ), 'type' => 'list', ) ); if ( ! $html ) { return ''; } return ''; } /* ---------------------------------------------------------------- * Asset enqueue * ---------------------------------------------------------------- */ /** * Loads the AJAX filter client. Only on archive screens and only * when AJAX mode is active. */ public function enqueue_filter_js() { $is_archive = is_post_type_archive( AumNexCart_Post_Type::POST_TYPE ) || is_tax( AumNexCart_Post_Type::TAXONOMY ); if ( ! $is_archive ) { return; } if ( 'ajax' !== AumNexCart_Settings::get( 'filter_mode', 'url' ) ) { return; } wp_enqueue_script( 'aumnexcart-filter', AUMNEXCART_URL . 'assets/js/frontend-filter.js', array(), AUMNEXCART_VERSION, true ); wp_localize_script( 'aumnexcart-filter', 'aumnexcartFilter', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::AJAX_ACTION ), 'action' => self::AJAX_ACTION, ) ); } }