renderBuySectionWrapperEnd();
}
public function renderVariationDisplay($atts = [])
{
// Hand off rendering to the advanced-variation selector when the
// product is configured for advanced variations. The handler returns
// the filtered array with rendered=true after emitting its markup; if
// no listener handles it (e.g. an unconfigured advanced product), the
// filter is a no-op and we fall through to the simple-variation
// rendering below.
if ($this->product->detail->variation_type === Helper::PRODUCT_TYPE_ADVANCE_VARIATION) {
$result = apply_filters('fluent_cart/product/render_advanced_variation', [
'product' => $this->product,
'selector_style' => Arr::get($atts, 'selector_style', 'auto'),
'rendered' => false,
]);
if (!empty($result['rendered'])) {
return;
}
}
$otherInfo = (array)Arr::get($this->product->detail, 'other_info');
$groupBy = Arr::get($otherInfo, 'group_pricing_by', 'repeat_interval'); //repeat_interval,payment_type,none
if (count($this->paymentTypes) === 1 || $groupBy === 'none') {
$this->renderVariants(Arr::get($atts, 'variation_atts', []));
} else {
$this->renderTab(Arr::get($atts, 'variation_atts', []));
}
}
public function renderGalleryThumb()
{
$thumbnails = [];
$featuredMedia = $this->product->thumbnail ?? Vite::getAssetUrl('images/placeholder.svg');
// thumbnail can be an empty string (not null), so ?? above doesn't catch
// it — fall back to the placeholder so $featuredMedia is always a usable
// image URL for both the main and data-default-image-url.
if (!$featuredMedia || !\is_string($featuredMedia)) {
$featuredMedia = Vite::getAssetUrl('images/placeholder.svg');
}
$galleryImage = get_post_meta($this->product->ID, 'fluent-products-gallery-image', true);
if (!empty($galleryImage)) {
$thumbnails[0] = [
'media' => $galleryImage,
];
}
foreach ($this->variants as $variant) {
if (!empty($variant['media']['meta_value'])) {
$thumbnails[$variant['id']] = [
'media' => $variant['media']['meta_value'],
];
} else {
$this->defaultImageUrl = $featuredMedia;
$this->defaultImageAlt = Arr::get($variant, 'variation_title', '');
}
}
$images = empty($thumbnails) ? [] : $thumbnails;
$this->images = $images;
if (!empty($images)) {
$imageId = $this->defaultGalleryImageId;
if (isset($images[$imageId])) {
$imageMetaValue = $images[$imageId];
$this->defaultImageUrl = Arr::get($imageMetaValue, 'media.0.url', '');
$this->defaultImageAlt = Arr::get($imageMetaValue, 'media.0.title', '');
} else {
// Fallback to the first available thumbnail. Advanced-variation
// products often have per-variant images but no explicit
// default_variation_id — the thumbnails are keyed by real
// variant IDs while defaultGalleryImageId is 0, so the lookup
// above misses and the main image area renders blank with a
// broken-image icon.
$firstImage = reset($images);
$fallbackUrl = Arr::get($firstImage, 'media.0.url', '');
$this->defaultImageUrl = $fallbackUrl ?: ($featuredMedia ?: '');
$this->defaultImageAlt = $this->defaultImageAlt ?: Arr::get($firstImage, 'media.0.title', '');
}
}
// Nothing set a main image — e.g. a product with no variants, or no
// variant/gallery media. Fall back to the featured/placeholder image so
// the main area shows the placeholder instead of a broken .
if (empty($this->defaultImageUrl)) {
$this->defaultImageUrl = $featuredMedia;
}
?>
images, 'media.*.url');
if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1){
return '';
}
// Build variantTermMap and variantFirstMediaMap.
// For advanced variations, Pro builds both maps via the gallery_variation_data
// filter — it has access to AttributeGroup types (color/image) that free cannot
// query directly. For all other product types, free builds variantFirstMediaMap
// from the already-loaded $this->images; variantTermMap stays empty.
$this->variantTermMap = [];
$variantFirstMediaMap = [];
if ($this->product->detail->variation_type === Helper::PRODUCT_TYPE_ADVANCE_VARIATION) {
$galleryVariationData = apply_filters('fluent_cart/product/gallery_variation_data', [
'variant_term_map' => [],
'variant_first_media_map' => [],
], $this->product);
$this->variantTermMap = (array) Arr::get($galleryVariationData, 'variant_term_map', []);
$variantFirstMediaMap = (array) Arr::get($galleryVariationData, 'variant_first_media_map', []);
} else {
foreach ($this->images as $imageId => $image) {
if (empty($image['media']) || !is_array($image['media'])) {
continue;
}
$firstItem = $image['media'][0] ?? null;
if (!$firstItem) {
continue;
}
$firstUrl = Arr::get($firstItem, 'url', '');
$firstMediaId = (int) Arr::get($firstItem, 'id', 0);
if ($firstUrl) {
$variantFirstMediaMap[(int) $imageId] = [
'id' => $firstMediaId,
'url' => $firstUrl,
];
}
}
}
// Collect ALL gallery images as JSON for lightbox (even when max thumbnails limits visible thumbs).
// For advanced variations, deduplicate by media ID (when > 0) or by URL (for imported images).
$allGalleryImages = [];
$addedMediaKeys = [];
$isAdvVariation = !empty($this->variantTermMap);
foreach ($this->images as $imageId => $image) {
if (empty($image['media']) || !is_array($image['media'])) {
continue;
}
foreach ($image['media'] as $item) {
$url = Arr::get($item, 'url', '');
$mediaId = (int) Arr::get($item, 'id', 0);
if (empty($url)) {
continue;
}
if ($isAdvVariation) {
$dedupeKey = $mediaId > 0 ? 'i:' . $mediaId : 'u:' . $url;
if (isset($addedMediaKeys[$dedupeKey])) {
continue; // skip — already added this image
}
$addedMediaKeys[$dedupeKey] = true;
}
$allGalleryImages[] = [
'url' => $url,
'title' => Arr::get($item, 'title', ''),
'variation_id' => (string) $imageId,
'term_id' => (int) ($this->variantTermMap[(int) $imageId] ?? 0),
'media_id' => $mediaId,
];
}
}
?>
renderGalleryThumbControl($maxThumbnails); ?>
variantTermMap);
$countedMediaKeys = [];
$count = 0;
$totalImages = 0;
// Count unique images to render. For advanced variations, deduplicate by WP media ID
// when available, or by URL for externally imported images (media ID = 0).
foreach ($this->images as $imageId => $image) {
if (empty($image['media']) || !is_array($image['media'])) {
continue;
}
foreach ($image['media'] as $item) {
$url = Arr::get($item, 'url', '');
$mediaId = (int) Arr::get($item, 'id', 0);
if (empty($url)) {
continue;
}
if ($isAdvVariation) {
$mediaDedupeKey = $mediaId > 0 ? 'i:' . $mediaId : 'u:' . $url;
if (isset($countedMediaKeys[$mediaDedupeKey])) {
continue;
}
$countedMediaKeys[$mediaDedupeKey] = true;
}
$totalImages++;
}
}
$renderedMediaKeys = [];
// Render up to max; skip already-rendered media for advanced variation products.
foreach ($this->images as $imageId => $image) {
if (empty($image['media']) || !is_array($image['media'])) {
continue;
}
foreach ($image['media'] as $item) {
$url = Arr::get($item, 'url', '');
$mediaId = (int) Arr::get($item, 'id', 0);
if (empty($url)) {
continue;
}
if ($isAdvVariation) {
$mediaDedupeKey = $mediaId > 0 ? 'i:' . $mediaId : 'u:' . $url;
if (isset($renderedMediaKeys[$mediaDedupeKey])) {
continue;
}
$renderedMediaKeys[$mediaDedupeKey] = true;
}
if ($maxThumbnails !== null && $count >= (int) $maxThumbnails) {
$this->renderGallerySeeMoreButton($totalImages - (int) $maxThumbnails);
return;
}
$termId = (int) ($this->variantTermMap[(int) $imageId] ?? 0);
$this->renderGalleryThumbControlButton($item, $imageId, $termId, $mediaId);
$count++;
}
}
}
public function renderGallerySeeMoreButton($remainingCount)
{
?>
defaultVariationId ? 'is-hidden' : '';
$itemUrl = Arr::get($item, 'url', '');
$itemTitle = Arr::get($item, 'title', '');
$isSelected = !$this->galleryActiveSet && $imageId == $this->defaultGalleryImageId;
if ($isSelected) {
$this->galleryActiveSet = true;
}
?>
'all', // horizontal, vertical
'thumb_position' => 'bottom', // bottom, left, right, top
'scrollable_thumbs' => 'no', // yes / no
'max_thumbnails' => null, // null = no limit, integer = max visible
];
$atts = wp_parse_args($args, $defaults);
$thumbnailMode = $atts['thumbnail_mode'];
$wrapperAtts = [
'class' => 'fct-product-gallery-wrapper ' . 'thumb-pos-' . $atts['thumb_position'] . ' thumb-mode-' . $thumbnailMode,
'data-fct-product-gallery' => '',
'data-fluent-cart-product-gallery-wrapper' => '',
'data-thumbnail-mode' => $thumbnailMode,
'data-product-id' => $this->product->ID,
'data-scrollable-thumbs' => $atts['scrollable_thumbs'],
];
?>
product->detail->getStockAvailability();
if (!Arr::get($stockAvailability, 'manage_stock')) {
return '';
}
$isStock = $this->product->isStock();
// Check default variant stock for both simple and variable products
if ($this->defaultVariant) {
$isStock = $isStock && $this->defaultVariant->isStock();
}
$stockLabel = Arr::get($stockAvailability, 'availability');
$statusClass = $stockAvailability['class'] ?? '';
// Optional per-status custom labels (e.g. set on the Bricks Product Stock
// element via the fluent_cart/product_stock_availability filter). Emitted as
// data-attributes so the frontend JS, which re-derives the badge text on load
// and on variant switches, prefers them over the generic label map instead of
// overwriting them. Absent for the default template, so behavior is unchanged.
$inStockText = Arr::get($stockAvailability, 'in_stock_text');
$outOfStockText = Arr::get($stockAvailability, 'out_of_stock_text');
// The variant-level check above can override the aggregate stock_availability
// used for $stockLabel/$statusClass (e.g. this specific default variant is out
// of stock even though the product overall has other in-stock variants) — keep
// the label and class in sync so the badge never shows mismatched text/color.
// Honor the custom out-of-stock label here too so it survives this override on
// first load, matching what the frontend JS shows after a variant switch.
if (!$isStock) {
$statusClass = 'out-of-stock';
$stockLabel = !empty($outOfStockText) ? $outOfStockText : __('Out of Stock', 'fluent-cart');
}
$badgeAttributes = '';
if (!empty($inStockText)) {
$badgeAttributes .= sprintf(' data-in-stock-text="%s"', esc_attr($inStockText));
}
if (!empty($outOfStockText)) {
$badgeAttributes .= sprintf(' data-out-of-stock-text="%s"', esc_attr($outOfStockText));
}
echo sprintf(
'
%3$s
%4$s
',
esc_attr($statusClass),
$wrapper_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
esc_html__('Availability:', 'fluent-cart'),
esc_html($stockLabel),
$badgeAttributes // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- values passed through esc_attr() above
);
}
public function renderSku($wrapper_attributes = '', $showLabel = true, $label = '', $variant = null)
{
if (!$label) {
$label = __('SKU:', 'fluent-cart');
}
$labelHtml = '';
if ($showLabel && $label) {
$labelHtml = sprintf('%s ', esc_html($label));
}
if ($variant) {
if (empty($variant->sku)) {
return;
}
echo sprintf(
'