$max ) { return (int) $max; } return $value; } /** * Pick a value from an allowlist. * * @param mixed $value Raw value. * @param string[] $allowed Allowed values. * @param string $fallback Fallback. * @return string */ public static function pickFrom( $value, array $allowed, $fallback ) { $value = is_string( $value ) ? trim( $value ) : ''; return in_array( $value, $allowed, true ) ? $value : $fallback; } /** * Sanitize an array of positive integer IDs. * * @param mixed $value Raw array. * @return int[] */ public static function intArray( $value ) { if ( ! is_array( $value ) ) { return []; } $out = []; foreach ( $value as $item ) { $id = absint( $item ); if ( $id > 0 ) { $out[] = $id; } } return array_values( array_unique( $out ) ); } /** * Normalize and sanitize the full attribute set into a safe, typed array. * * @param array $attributes Raw block attributes. * @return array */ public static function resolveAttributes( array $attributes ) { $columns = (array) ( $attributes['columns'] ?? [] ); $titleFont = (array) ( $attributes['titleFontSize'] ?? [] ); $saleBadgeLabel = isset( $attributes['saleBadgeLabel'] ) ? wp_strip_all_tags( (string) $attributes['saleBadgeLabel'] ) : ''; $saleBadgeLabel = '' !== trim( $saleBadgeLabel ) ? $saleBadgeLabel : __( 'Sale', 'b-blocks' ); $addToCartLabel = isset( $attributes['addToCartLabel'] ) ? wp_strip_all_tags( (string) $attributes['addToCartLabel'] ) : ''; $addToCartLabel = '' !== trim( $addToCartLabel ) ? $addToCartLabel : __( 'Add to Cart', 'b-blocks' ); $noProductsMessage = isset( $attributes['noProductsMessage'] ) ? wp_strip_all_tags( (string) $attributes['noProductsMessage'] ) : ''; $noProductsMessage = '' !== trim( $noProductsMessage ) ? $noProductsMessage : __( 'No products found.', 'b-blocks' ); return [ 'columnsDesktop' => self::clampInt( $columns['desktop'] ?? 3, 1, 6, 3 ), 'columnsTablet' => self::clampInt( $columns['tablet'] ?? 2, 1, 6, 2 ), 'columnsMobile' => self::clampInt( $columns['mobile'] ?? 1, 1, 6, 1 ), 'columnGap' => self::clampInt( $attributes['columnGap'] ?? 20, 0, 60, 20 ), 'rowGap' => self::clampInt( $attributes['rowGap'] ?? 20, 0, 60, 20 ), 'productsPerPage' => self::clampInt( $attributes['productsPerPage'] ?? 9, 1, 48, 9 ), 'orderBy' => self::pickFrom( $attributes['orderBy'] ?? 'date', self::ORDERBY, 'date' ), 'order' => self::pickFrom( strtoupper( (string) ( $attributes['order'] ?? 'desc' ) ), self::ORDER, 'DESC' ), 'productCategories' => self::intArray( $attributes['productCategories'] ?? [] ), 'productTags' => self::intArray( $attributes['productTags'] ?? [] ), 'onSaleOnly' => ! empty( $attributes['onSaleOnly'] ), 'featuredOnly' => ! empty( $attributes['featuredOnly'] ), 'showImage' => ! isset( $attributes['showImage'] ) || (bool) $attributes['showImage'], 'imageFit' => self::pickFrom( $attributes['imageFit'] ?? 'cover', [ 'cover', 'contain' ], 'cover' ), 'imageRatio' => self::pickFrom( $attributes['imageRatio'] ?? '3/4', self::RATIOS, '3/4' ), 'showTitle' => ! isset( $attributes['showTitle'] ) || (bool) $attributes['showTitle'], 'showPrice' => ! isset( $attributes['showPrice'] ) || (bool) $attributes['showPrice'], 'showRating' => ! isset( $attributes['showRating'] ) || (bool) $attributes['showRating'], 'showSaleBadge' => ! isset( $attributes['showSaleBadge'] ) || (bool) $attributes['showSaleBadge'], 'saleBadgeLabel' => $saleBadgeLabel, 'showAddToCart' => ! isset( $attributes['showAddToCart'] ) || (bool) $attributes['showAddToCart'], 'addToCartLabel' => $addToCartLabel, 'contentAlign' => self::pickFrom( $attributes['contentAlign'] ?? 'left', [ 'left', 'center', 'right' ], 'left' ), 'cardBG' => self::sanitizeColor( $attributes['cardBG'] ?? '', '#ffffff' ), 'cardPadding' => self::clampInt( $attributes['cardPadding'] ?? 16, 0, 48, 16 ), 'cardBorderWidth' => self::clampInt( $attributes['cardBorderWidth'] ?? 1, 0, 8, 1 ), 'cardBorderColor' => self::sanitizeColor( $attributes['cardBorderColor'] ?? '', '#e2e8f0' ), 'cardRadius' => self::clampInt( $attributes['cardRadius'] ?? 8, 0, 32, 8 ), 'cardShadow' => self::pickFrom( $attributes['cardShadow'] ?? 'none', [ 'none', 'sm', 'md', 'lg' ], 'none' ), 'titleColor' => self::sanitizeColor( $attributes['titleColor'] ?? '', 'inherit' ), 'priceColor' => self::sanitizeColor( $attributes['priceColor'] ?? '', '#e44d3a' ), 'regularPriceColor' => self::sanitizeColor( $attributes['regularPriceColor'] ?? '', '#999999' ), 'ratingColor' => self::sanitizeColor( $attributes['ratingColor'] ?? '', '#f5a623' ), 'badgeBG' => self::sanitizeColor( $attributes['badgeBG'] ?? '', '#e44d3a' ), 'badgeTextColor' => self::sanitizeColor( $attributes['badgeTextColor'] ?? '', '#ffffff' ), 'btnColor' => self::sanitizeColor( $attributes['btnColor'] ?? '', '#ffffff' ), 'btnBG' => self::sanitizeColor( $attributes['btnBG'] ?? '', '#146EF5' ), 'btnHovColor' => self::sanitizeColor( $attributes['btnHovColor'] ?? '', '#ffffff' ), 'btnHovBG' => self::sanitizeColor( $attributes['btnHovBG'] ?? '', '#070127' ), 'btnRadius' => self::clampInt( $attributes['btnRadius'] ?? 4, 0, 32, 4 ), 'titleSizeDesktop' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['desktop'] ?? '17' ) ), 12, 40, 17 ), 'titleSizeTablet' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['tablet'] ?? '16' ) ), 12, 36, 16 ), 'titleSizeMobile' => self::clampInt( preg_replace( '/[^0-9]/', '', (string) ( $titleFont['mobile'] ?? '15' ) ), 12, 32, 15 ), 'noProductsMessage' => $noProductsMessage, ]; } /* ---------------------------------------------------------------------- * Query * ------------------------------------------------------------------- */ /** * Build sanitized wc_get_products() args. * * @param array $a Resolved attributes. * @return array */ public static function buildQueryArgs( array $a ) { // Map our orderby to WooCommerce-recognized values. $orderByMap = [ 'date' => 'date', 'price' => 'price', 'rating' => 'rating', 'popularity' => 'popularity', 'rand' => 'rand', 'title' => 'title', ]; $args = [ 'status' => 'publish', 'limit' => $a['productsPerPage'], 'orderby' => $orderByMap[ $a['orderBy'] ] ?? 'date', 'order' => $a['order'], 'paginate' => false, 'return' => 'objects', ]; if ( ! empty( $a['productCategories'] ) ) { $args['category'] = self::termIdsToSlugs( $a['productCategories'], 'product_cat' ); } if ( ! empty( $a['productTags'] ) ) { $args['tag'] = self::termIdsToSlugs( $a['productTags'], 'product_tag' ); } if ( $a['featuredOnly'] ) { $args['featured'] = true; } if ( $a['onSaleOnly'] && function_exists( 'wc_get_product_ids_on_sale' ) ) { $onSale = wc_get_product_ids_on_sale(); // Empty include with on-sale-only means no products; use a sentinel. $args['include'] = ! empty( $onSale ) ? $onSale : [ 0 ]; } return $args; } /** * Convert term IDs to slugs for a taxonomy (wc_get_products expects slugs). * * @param int[] $ids Term IDs. * @param string $taxonomy Taxonomy. * @return string[] */ protected static function termIdsToSlugs( array $ids, $taxonomy ) { $slugs = []; foreach ( $ids as $id ) { $term = get_term( (int) $id, $taxonomy ); if ( $term && ! is_wp_error( $term ) ) { $slugs[] = $term->slug; } } return $slugs; } /* ---------------------------------------------------------------------- * Card rendering * ------------------------------------------------------------------- */ /** * Render the product cards as an escaped HTML fragment. * * @param \WC_Product[] $products Products. * @param array $a Resolved attributes. * @return string Escaped HTML. */ public static function renderCards( array $products, array $a ) { ob_start(); foreach ( $products as $product ) : if ( ! is_a( $product, 'WC_Product' ) ) { continue; } $productId = $product->get_id(); $titleText = $product->get_name(); $permalink = get_permalink( $productId ); $isOnSale = $product->is_on_sale(); $isSimple = $product->is_type( 'simple' ); $canAjax = $isSimple && $product->is_purchasable() && $product->is_in_stock(); // Image. $imageUrl = ''; $imageAlt = $titleText; if ( $a['showImage'] ) { $thumbId = $product->get_image_id(); if ( $thumbId ) { $src = wp_get_attachment_image_url( $thumbId, 'woocommerce_thumbnail' ); if ( $src ) { $imageUrl = $src; $metaAlt = get_post_meta( $thumbId, '_wp_attachment_image_alt', true ); if ( is_string( $metaAlt ) && '' !== trim( $metaAlt ) ) { $imageAlt = trim( wp_strip_all_tags( $metaAlt ) ); } } } if ( '' === $imageUrl && function_exists( 'wc_placeholder_img_src' ) ) { $imageUrl = wc_placeholder_img_src( 'woocommerce_thumbnail' ); } } // Rating. $ratingValue = (float) $product->get_average_rating(); $ratingCount = (int) $product->get_rating_count(); // Add-to-cart label. $cartLabel = $a['addToCartLabel']; if ( ! $isSimple ) { $wcLabel = $product->add_to_cart_text(); if ( is_string( $wcLabel ) && '' !== trim( $wcLabel ) ) { $cartLabel = wp_strip_all_tags( $wcLabel ); } } ?>
'>

0 ) : ?> = $i ) { $starClass .= ' is-full'; } elseif ( $roundedRating >= ( $i - 0.5 ) ) { $starClass .= ' is-half'; } echo ''; } ?>
get_price_html() ); ?>
cart ) { wp_send_json_error( [ 'message' => __( 'WooCommerce is not available.', 'b-blocks' ) ] ); } $productId = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0; if ( $productId < 1 ) { wp_send_json_error( [ 'message' => __( 'Invalid product.', 'b-blocks' ) ] ); } $product = wc_get_product( $productId ); if ( ! $product || ! is_a( $product, 'WC_Product' ) ) { wp_send_json_error( [ 'message' => __( 'Product not found.', 'b-blocks' ) ] ); } if ( ! $product->is_type( 'simple' ) || ! $product->is_purchasable() || ! $product->is_in_stock() ) { wp_send_json_error( [ 'message' => __( 'This product cannot be added to the cart.', 'b-blocks' ) ] ); } $added = WC()->cart->add_to_cart( $productId, 1 ); if ( ! $added ) { wp_send_json_error( [ 'message' => __( 'Could not add the product to the cart.', 'b-blocks' ) ] ); } wp_send_json_success( [ 'added' => true, 'productName' => wp_strip_all_tags( $product->get_name() ), 'cartCount' => WC()->cart->get_cart_contents_count(), ] ); } } new WooProductGrid();