(no `products` class: WooCommerce/theme loop floats stay out)
*
* Sale!
* img …
* stars
…
* WooCommerce loop add-to-cart button (AJAX for simple products)
*
*
*
* The add-to-cart button is WooCommerce's own (`woocommerce_template_loop_add_to_cart`), so its AJAX add to cart,
* `added_to_cart` event and cart fragments work as on the shop page. Parts are toggled by the settings
* show_image, show_title, show_rating, show_price, show_button, show_badge.
*/
final class Cards
{
/**
* @param iterable<\WC_Product> $products
* @param array $settings
*/
public static function render(iterable $products, array $settings, string $block = 'zx-products'): string
{
$columns = max(1, min(8, (int) (is_array($settings['columns'] ?? null) ? ($settings['columns']['size'] ?? 4) : ($settings['columns'] ?? 4))));
$items = '';
foreach ($products as $product) {
if (Product::is_product($product)) {
$items .= self::card($product, $settings);
}
}
if ($items === '') {
return '';
}
return '';
}
/** @param \WC_Product $product */
public static function card(object $product, array $settings): string
{
$on = static fn(string $key): bool => !array_key_exists($key, $settings) || !empty($settings[$key]);
$tag = in_array($settings['title_tag'] ?? '', ['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div'], true) ? $settings['title_tag'] : 'h3';
$size = is_string($settings['image_size'] ?? null) && $settings['image_size'] !== '' ? sanitize_key($settings['image_size']) : 'woocommerce_thumbnail';
$classes = function_exists('wc_get_product_class') ? (array) wc_get_product_class('zx-product-card', $product) : ['zx-product-card', 'product'];
$url = (string) $product->get_permalink();
$html = '';
$html .= '';
if ($on('show_badge') && $product->is_on_sale()) {
$text = is_string($settings['badge_text'] ?? null) && $settings['badge_text'] !== '' ? $settings['badge_text'] : __('Sale!', 'zadex');
$html .= '' . esc_html($text) . '';
}
if ($on('show_image')) {
$html .= '' . $product->get_image($size) . '';
}
if ($on('show_title')) {
$html .= '<' . $tag . ' class="zx-product-card__title woocommerce-loop-product__title">' . esc_html($product->get_name()) . '' . $tag . '>';
}
$html .= '';
if ($on('show_rating') && function_exists('wc_get_rating_html')) {
$rating = wc_get_rating_html((float) $product->get_average_rating(), (int) $product->get_rating_count());
if ($rating !== '') {
$html .= '' . $rating . '
';
}
}
if ($on('show_price')) {
$price = (string) $product->get_price_html();
if ($price !== '') {
$html .= '' . $price . '';
}
}
if ($on('show_button') && function_exists('woocommerce_template_loop_add_to_cart')) {
$button = Product::with($product, static function (object $p): void {
$buyable = $p->is_purchasable() && $p->is_in_stock();
woocommerce_template_loop_add_to_cart(['class' => implode(' ', array_filter([
'button', 'zx-product-card__add', 'wp-element-button', 'has-text-color', 'has-background', 'product_type_' . $p->get_type(),
$buyable ? 'add_to_cart_button' : '',
$buyable && $p->supports('ajax_add_to_cart') ? 'ajax_add_to_cart' : '',
]))]);
});
$html .= '' . $button . '
';
}
return $html . '';
}
/** Pagination links for a query (`?product-page=N`). */
public static function pagination(int $total_pages, int $current, bool $archive = false): string
{
if ($total_pages < 2 || !function_exists('paginate_links')) {
return '';
}
$args = [
'total' => $total_pages,
'current' => max(1, $current),
'type' => 'list',
'prev_text' => is_rtl() ? '→' : '←',
'next_text' => is_rtl() ? '←' : '→',
];
if (!$archive) {
$args['format'] = '?' . ProductsQuery::PAGE_VAR . '=%#%';
$args['base'] = esc_url_raw(add_query_arg(ProductsQuery::PAGE_VAR, '%#%', false));
}
$links = (string) paginate_links($args);
return $links !== '' ? '' : '';
}
}