product = $product; $this->viewUrl = $product->view_url; $this->config = $config; } public function renderWrapperStart() { } public function renderWrapperEnd() { } public function render() { AssetLoader::loadSingleProductAssets(); // Lets extensions (e.g. Pro's advanced-variation selector) enqueue the // CSS/JS a product purchase UI needs. Backs the card surfaces that go // through this renderer's render() — Shop, Product List, the product // shortcode, and the quick-view modal. Surfaces that render card parts // via the individual render* methods instead of render() (Product // Carousel, Related Products) fire this same hook from their own block // render. Guarded with a static flag so a grid of N cards fires it once // per request, not once per card. No payload — assets are uniform. static $assetsHookFired = false; if (!$assetsHookFired) { $assetsHookFired = true; do_action('fluent_cart/advanced_variation/enqueue_assets'); } $cursor = ''; if (!empty($this->config['cursor'])) { $cursor = 'data-fluent-cart-cursor="' . esc_attr($this->config['cursor']) . '"'; } $cardWidth = ''; $cardWidthValue = Arr::get($this->config, 'card_width', ''); if ($cardWidthValue) { $widthValue = $cardWidthValue === '100%' ? '100%' : intval($cardWidthValue) . 'px'; $cardWidth = 'style="width: ' . esc_attr($widthValue) . ';"'; } ?>
aria-label="product->post_title)); ?>"> renderProductImage(); ?> renderTitle(); ?> renderExcerpt(); ?> renderPrices(); ?> showBuyButton(); ?>
product->variants->first(); if (!$variant) { return; } if (!$wrapper_attributes) { $wrapper_attributes = 'class="fct-package-description" data-fluent-cart-package-description'; } $this->renderPackageDescriptionForVariant($variant, $wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight); } private function renderPackageDescriptionForVariant( ProductVariation $variant, $wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight ) { if ($variant->fulfillment_type !== 'physical') { return; } $packageSlug = Arr::get($variant->other_info, 'package_slug', ''); $package = Helper::getPackageBySlug($packageSlug); if (!$package) { return; } $name = Arr::get($package, 'name', ''); $length = Arr::get($package, 'length', ''); $width = Arr::get($package, 'width', ''); $height = Arr::get($package, 'height', ''); $dimensionUnit = Arr::get($package, 'dimension_unit', 'cm'); $packageWeight = floatval(Arr::get($package, 'weight', 0)); $packageWeightUnit = Arr::get($package, 'weight_unit', 'kg'); $storeWeightUnit = Helper::shopConfig('weight_unit') ?: 'kg'; $otherInfo = $variant->other_info ?: []; $productWeight = floatval(Arr::get($otherInfo, 'weight', 0)); $productWeightUnit = Arr::get($otherInfo, 'weight_unit', $storeWeightUnit); $convertedProductWeight = Helper::convertWeight($productWeight, $productWeightUnit, $storeWeightUnit); $convertedPackageWeight = Helper::convertWeight($packageWeight, $packageWeightUnit, $storeWeightUnit); $totalWeight = $convertedProductWeight + $convertedPackageWeight; $hasVisibleContent = ($showName && $name) || ($showDimensions && ($length || $width || $height)) || ($showProductWeight && $convertedProductWeight) || ($showTotalWeight && $totalWeight); if (!$hasVisibleContent) { return; } echo sprintf('
', $wrapper_attributes); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; if ($showName && $name) { echo sprintf( '', esc_html__('Package', 'fluent-cart'), esc_html($name) ); } if ($showDimensions && ($length || $width || $height)) { $dimensionParts = array_filter([$length, $width, $height], function ($val) { return $val !== '' && $val !== null; }); if ($dimensionParts) { $formattedDimensions = implode(' × ', $dimensionParts) . ' ' . $dimensionUnit; echo sprintf( '', esc_html__('Dimensions', 'fluent-cart'), esc_html($formattedDimensions) ); } } if ($showProductWeight && $convertedProductWeight) { $formattedProductWeight = rtrim(rtrim(number_format($convertedProductWeight, 2), '0'), '.'); echo sprintf( '', esc_html__('Weight', 'fluent-cart'), esc_html($formattedProductWeight . ' ' . $storeWeightUnit) ); } if ($showTotalWeight && $totalWeight && $convertedPackageWeight) { $formattedTotalWeight = rtrim(rtrim(number_format($totalWeight, 2), '0'), '.'); echo sprintf( '', esc_html__('Shipping Weight', 'fluent-cart'), esc_html($formattedTotalWeight . ' ' . $storeWeightUnit) ); } echo ''; echo '
'; } public function renderExcerpt($atts = '') { if (empty($this->product->post_excerpt)) { return; } echo sprintf( '

%2$s

', $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_kses_post($this->product->post_excerpt), ); } public function renderTitle($atts = '', $config = []) { $link = Arr::get($config, 'isLink', true); $target = Arr::get($config, 'target', '_self'); $titleText = esc_html($this->product->post_title); if ($link) { // Render as link $targetAttr = $target === '_blank' ? 'target="_blank" rel="noopener noreferrer"' : ''; echo sprintf( '

%6$s

', $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_attr($this->product->ID), esc_url($this->product->view_url), esc_attr($this->product->post_title), $targetAttr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html($titleText) ); } else { // Render as plain text (no link) echo sprintf( '

%2$s

', $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html($titleText) ); } } public function renderProductImage() { $image = $this->product->thumbnail; $isPlaceholder = false; if (!$image) { $image = Vite::getAssetUrl('images/placeholder.svg'); $isPlaceholder = true; } $altText = $isPlaceholder ? sprintf( /* translators: %s: product title */ __('Placeholder image for %s', 'fluent-cart'), $this->product->post_title) : $this->product->post_title; do_action('fluent_cart/product/group/before_image_block', [ 'product' => $this->product, 'scope' => 'product_card' ]); ?> <?php echo esc_attr($altText); ?> $this->product, 'scope' => 'product_card' ]); } public function renderPrices($wrapper_attributes = '') { $priceFormat = Arr::get($this->config, 'price_format', 'starts_from'); $isSimple = $this->product->detail->variation_type === 'simple'; $minPrice = $this->product->detail->min_price; $maxPrice = $this->product->detail->max_price; $comparePrice = 0; $firstVariant = null; if ($isSimple) { $firstVariant = $this->product->variants->first(); if ($firstVariant) { $minPrice = $firstVariant->item_price; $minPrice = apply_filters('fluent_cart/product/display_price', $minPrice, [ 'product' => $this->product, 'variation' => $firstVariant, ]); $minPrice = (int)$minPrice; if ($firstVariant->compare_price > $minPrice) { $comparePrice = $firstVariant->compare_price; } } } $formattedMinPrice = Helper::toDecimal($minPrice); $formattedMaxPrice = Helper::toDecimal($maxPrice); $formattedComparePrice = Helper::toDecimal($comparePrice); do_action('fluent_cart/product/group/before_price_block', [ 'product' => $this->product, 'current_price' => $minPrice, 'scope' => 'product_card' ]); ?>
class="fct-product-card-prices" role="region" aria-label=""> $minPrice): ?> $this->product, 'variant' => $firstVariant, 'current_price' => $minPrice, 'scope' => 'product_card' ]); ?>
$this->product, 'current_price' => $minPrice, 'scope' => 'product_card' ]); } public function showBuyButton($atts = '') { $isOutOfStock = ModuleSettings::isActive('stock_management') && !$this->product->isStock(); if ($isOutOfStock) { $soldOutText = __('Not Available', 'fluent-cart'); ?> product->detail->variation_type === 'simple'; $firstVariant = null; $buttonHref = $this->viewUrl; if ($isSimple) { $firstVariant = $this->product->variants->first(); if ($firstVariant) { // return ''; } } $isInstantCheckout = false; $hasSubscription = $this->product->has_subscription; $buttonText = __('View Options', 'fluent-cart'); $ariaLabel = sprintf( /* translators: %s: product title */ __('View options for %s', 'fluent-cart'), $this->product->post_title); if ($isSimple) { if ($hasSubscription) { $buttonText = __('Buy Now', 'fluent-cart'); $ariaLabel = sprintf( /* translators: %s: product title */ __('Buy %s now', 'fluent-cart'), $this->product->post_title); $buttonHref = $firstVariant->getPurchaseUrl(); $isInstantCheckout = true; } else { $buttonText = __('Add To Cart', 'fluent-cart'); $ariaLabel = sprintf( /* translators: %s: product title */ __('Add %s to cart', 'fluent-cart'), $this->product->post_title); } } $buttonAttributes = [ 'class' => 'fct-product-view-button fct-single-product-card-view-button', 'data-product-id' => $this->product->ID, 'data-fluent-cart-single-product-card-view-button' => '', 'aria-label' => $ariaLabel ]; if ($firstVariant) { $buttonAttributes = [ 'data-cart-id' => $firstVariant->id, 'class' => 'fluent-cart-add-to-cart-button', 'data-variation-type' => $this->product->detail->variation_type, 'data-fluent-cart-add-to-cart-button' => '', 'data-is-custom' => false, 'aria-label' => $ariaLabel ]; } $customAttributes = $this->parseAttributes($atts); if (!empty($customAttributes)) { if (isset($customAttributes['class']) && isset($buttonAttributes['class'])) { $buttonAttributes['class'] .= ' ' . $customAttributes['class']; unset($customAttributes['class']); } $buttonAttributes = array_merge($buttonAttributes, $customAttributes); } $anchorAttributes = [ 'href' => $buttonHref, 'class' => 'fct-product-view-button', 'aria-label' => $ariaLabel, ]; if ($enableModalCheckout) { $anchorAttributes['data-fct-instant-checkout-button'] = ''; $anchorAttributes['data-enable-modal-checkout'] = 'yes'; } if ($isInstantCheckout) { $parsedCustomAttributes = $this->parseAttributes($atts); if (isset($parsedCustomAttributes['class']) && isset($anchorAttributes['class'])) { $anchorAttributes['class'] .= ' ' . $parsedCustomAttributes['class']; unset($parsedCustomAttributes['class']); } $anchorAttributes = array_merge($anchorAttributes, $parsedCustomAttributes); } ?> renderAttributes($anchorAttributes); ?>> $value) { if ($value !== '') { echo esc_attr($attr) . '="' . esc_attr((string)$value) . '" '; } else { echo esc_attr($attr) . ' '; } } } private function parseAttributes($atts) { if (empty($atts)) { return []; } $attributes = []; // Match attribute="value" or attribute='value' or attribute=value preg_match_all('/(\w+(?:-\w+)*)=(["\'])(.*?)\2|\b(\w+(?:-\w+)*)=(\S+)/', $atts, $matches, PREG_SET_ORDER); foreach ($matches as $match) { if (!empty($match[1])) { // Quoted value $attributes[$match[1]] = $match[3]; } elseif (!empty($match[4])) { // Unquoted value $attributes[$match[4]] = $match[5]; } } return $attributes; } }