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) . ';"';
}
// Card wrapper classes are filterable so an integration can tag cards
// (sale, low-stock, layout variants) without wrapping or re-rendering
// the whole card. Sanitised in RenderGate, so listeners may return a
// plain array of class names.
$cardClasses = RenderGate::cardClasses(['fct-product-card'], [
'product' => $this->product,
'scope' => RenderGate::SCOPE_CARD,
]);
do_action('fluent_cart/product/group/before_card', RenderContext::decorate([
'product' => $this->product,
'scope' => RenderGate::SCOPE_CARD,
]));
?>
aria-label="product->post_title));
?>">
renderProductImage(); ?>
renderTitle(); ?>
config, 'hide_excerpt', false)) { $this->renderExcerpt(); } ?>
renderPrices(); ?>
showBuyButton(); ?>
$this->product,
'scope' => RenderGate::SCOPE_CARD,
]));
}
/**
* @deprecated Use PackageDescriptionRenderer::buildPackageInfoFromOtherInfo() directly.
* Kept as a compatibility shim for external callers (themes, extensions).
*
* @param array $otherInfo Order item's other_info array
* @return string e.g. "Gift (Box) · 30 × 20 × 15 cm · Wt: 2 kg · Shipping: 2.5 kg"
*/
public static function buildPackageInfoFromOtherInfo($otherInfo)
{
return PackageDescriptionRenderer::buildPackageInfoFromOtherInfo($otherInfo);
}
/**
* @deprecated Use PackageDescriptionRenderer::renderPackageDescription() directly.
* Kept as a compatibility shim for external callers (themes, extensions) —
* package-description rendering now lives in PackageDescriptionRenderer.
*/
public function renderPackageDescription(
$wrapper_attributes = '',
$showName = true,
$showDimensions = true,
$showProductWeight = true,
$showTotalWeight = true,
$variant = null,
$defaultVariant = null
) {
(new PackageDescriptionRenderer($this->product))->renderPackageDescription(
$wrapper_attributes,
$showName,
$showDimensions,
$showProductWeight,
$showTotalWeight,
$variant,
$defaultVariant
);
}
public function renderExcerpt($atts = '')
{
if (empty($this->product->post_excerpt)) {
return;
}
$gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
if (!RenderGate::shouldRender('excerpt', $gateContext)) {
return;
}
do_action('fluent_cart/product/group/before_excerpt_block', $gateContext);
echo sprintf(
'