| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace FluentCart\App\Modules\Templating\Bricks; |
| 4 | 4 | |
| 5 | 5 | use Bricks\Frontend; |
| 6 | +use FluentCart\App\Helpers\Helper; | |
| 6 | 7 | use FluentCart\Framework\Support\Arr; |
| 7 | 8 | |
| 8 | 9 | class BricksHelper |
| 9 | 10 | { |
| @@ -9,8 +10,16 @@ | ||
| 9 | 10 | { |
| 10 | 11 | |
| 11 | 12 | static public $forcedPost = null; |
| 12 | 13 | |
| 14 | + /** | |
| 15 | + * Settings of the Products Collection element currently being rendered. | |
| 16 | + * Exposed so DynamicData::renderValue() can read Sale/Sold Out badge | |
| 17 | + * settings when resolving the {fct_product_image} tag — that filter | |
| 18 | + * only receives ($tag, $post, $context), not the calling element. | |
| 19 | + */ | |
| 20 | + static public $imageBadgeSettings = []; | |
| 21 | + | |
| 13 | 22 | public static function getFormCurrentPost() |
| 14 | 23 | { |
| 15 | 24 | return self::$forcedPost; |
| 16 | 25 | } |
| @@ -39,10 +48,17 @@ | ||
| 39 | 48 | } |
| 40 | 49 | |
| 41 | 50 | public static function renderCollectionCard($settings, $post, $post_index = 1, $uid = '') |
| 42 | 51 | { |
| 52 | + self::$imageBadgeSettings = $settings; | |
| 53 | + | |
| 43 | 54 | $content = Frontend::get_content_wrapper($settings, Arr::get($settings, 'fields', []), $post); |
| 44 | 55 | |
| 56 | + // Scope the badge settings strictly to the get_content_wrapper() call above — | |
| 57 | + // {fct_product_image} is a generic dynamic tag usable outside Products Collection | |
| 58 | + // too, and it must not inherit stale settings from this render. | |
| 59 | + self::$imageBadgeSettings = []; | |
| 60 | + | |
| 45 | 61 | if ($post_index === 1) { |
| 46 | 62 | echo "<div data-fluent-client-id='" . esc_attr($uid) . "' data-template-provider='bricks' data-fct-product-card class='fct-product-card repeater-item'>"; |
| 47 | 63 | } else { |
| 48 | 64 | echo "<div data-fct-product-card class='fct-product-card repeater-item'>"; |
| @@ -60,6 +76,151 @@ | ||
| 60 | 76 | echo '</a>'; |
| 61 | 77 | } |
| 62 | 78 | |
| 63 | 79 | echo '</div>'; |
| 80 | + } | |
| 81 | + | |
| 82 | + public static function isTemplate() | |
| 83 | + { | |
| 84 | + $is_template = false; | |
| 85 | + | |
| 86 | + if (isset($_GET['bricks']) && $_GET['bricks'] === 'run') { | |
| 87 | + $request_uri = $_SERVER['REQUEST_URI'] ?? ''; | |
| 88 | + $is_template = strpos($request_uri, '/template/') !== false; | |
| 89 | + } | |
| 90 | + | |
| 91 | + return $is_template; | |
| 92 | + } | |
| 93 | + | |
| 94 | + /** | |
| 95 | + * Returns concatenated Sale Badge / Sold Out Badge <span> markup for | |
| 96 | + * $product based on $settings (the Products Collection element's | |
| 97 | + * settings array). Mirrors the detection logic used by core's | |
| 98 | + * SaleBadgeBlockEditor::render() / SoldOutBadgeBlockEditor::render(). | |
| 99 | + */ | |
| 100 | + public static function renderProductBadges($product, array $settings) | |
| 101 | + { | |
| 102 | + if (!$product) { | |
| 103 | + return ''; | |
| 104 | + } | |
| 105 | + | |
| 106 | + return self::renderSaleBadge($product, $settings) . self::renderSoldOutBadge($product, $settings); | |
| 107 | + } | |
| 108 | + | |
| 109 | + private static function renderSaleBadge($product, array $settings) | |
| 110 | + { | |
| 111 | + if (empty($settings['showSaleBadge'])) { | |
| 112 | + return ''; | |
| 113 | + } | |
| 114 | + | |
| 115 | + if (!$product->variants || $product->variants->isEmpty()) { | |
| 116 | + return ''; | |
| 117 | + } | |
| 118 | + | |
| 119 | + $priceSource = Arr::get($settings, 'saleBadgePriceSource', 'default_variant'); | |
| 120 | + if (!in_array($priceSource, ['default_variant', 'best_discount'], true)) { | |
| 121 | + $priceSource = 'default_variant'; | |
| 122 | + } | |
| 123 | + | |
| 124 | + $isOnSale = false; | |
| 125 | + $discountPercent = 0; | |
| 126 | + | |
| 127 | + if ($priceSource === 'default_variant') { | |
| 128 | + $defaultVariantId = $product->detail->default_variation_id ?? null; | |
| 129 | + | |
| 130 | + $variant = $defaultVariantId | |
| 131 | + ? ($product->variants->firstWhere('id', $defaultVariantId) ?? $product->variants->first()) | |
| 132 | + : $product->variants->first(); | |
| 133 | + | |
| 134 | + if ($variant && $variant->compare_price > $variant->item_price && $variant->compare_price > 0) { | |
| 135 | + $isOnSale = true; | |
| 136 | + $discountPercent = max(0, min(100, round((($variant->compare_price - $variant->item_price) / $variant->compare_price) * 100))); | |
| 137 | + } | |
| 138 | + } else { | |
| 139 | + foreach ($product->variants as $variant) { | |
| 140 | + if ($variant->compare_price > $variant->item_price && $variant->compare_price > 0) { | |
| 141 | + $isOnSale = true; | |
| 142 | + $discount = max(0, min(100, round((($variant->compare_price - $variant->item_price) / $variant->compare_price) * 100))); | |
| 143 | + if ($discount > $discountPercent) { | |
| 144 | + $discountPercent = $discount; | |
| 145 | + } | |
| 146 | + } | |
| 147 | + } | |
| 148 | + } | |
| 149 | + | |
| 150 | + if (!$isOnSale) { | |
| 151 | + return ''; | |
| 152 | + } | |
| 153 | + | |
| 154 | + $showPercentage = !empty($settings['saleBadgeShowPercentage']); | |
| 155 | + $badgeText = sanitize_text_field(Arr::get($settings, 'saleBadgeText', '')) ?: __('Sale!', 'fluent-cart'); | |
| 156 | + $percentageText = sanitize_text_field(Arr::get($settings, 'saleBadgePercentageText', '')) ?: '-{percent}%'; | |
| 157 | + | |
| 158 | + if ($showPercentage && $discountPercent > 0) { | |
| 159 | + $displayText = str_replace('{percent}', $discountPercent, $percentageText); | |
| 160 | + } else { | |
| 161 | + $displayText = $badgeText; | |
| 162 | + } | |
| 163 | + | |
| 164 | + $badgeShape = Arr::get($settings, 'saleBadgeShape', 'badge'); | |
| 165 | + if (!in_array($badgeShape, ['badge', 'ribbon'], true)) { | |
| 166 | + $badgeShape = 'badge'; | |
| 167 | + } | |
| 168 | + | |
| 169 | + $badgePosition = Arr::get($settings, 'saleBadgePosition', 'top-left'); | |
| 170 | + if (!in_array($badgePosition, ['top-left', 'top-right', 'bottom-left', 'bottom-right'], true)) { | |
| 171 | + $badgePosition = 'top-left'; | |
| 172 | + } | |
| 173 | + | |
| 174 | + $classes = 'fct-sale-badge fct-sale-badge--' . $badgeShape . ' fct-sale-badge--' . $badgePosition; | |
| 175 | + | |
| 176 | + return '<span class="' . esc_attr($classes) . '">' . esc_html($displayText) . '</span>'; | |
| 177 | + } | |
| 178 | + | |
| 179 | + private static function renderSoldOutBadge($product, array $settings) | |
| 180 | + { | |
| 181 | + if (empty($settings['showSoldOutBadge'])) { | |
| 182 | + return ''; | |
| 183 | + } | |
| 184 | + | |
| 185 | + if (!$product->detail) { | |
| 186 | + return ''; | |
| 187 | + } | |
| 188 | + | |
| 189 | + $isOutOfStock = $product->detail->stock_availability === Helper::OUT_OF_STOCK; | |
| 190 | + if (!$isOutOfStock) { | |
| 191 | + return ''; | |
| 192 | + } | |
| 193 | + | |
| 194 | + $badgeText = sanitize_text_field(Arr::get($settings, 'soldOutBadgeText', '')) ?: __('Out of Stock', 'fluent-cart'); | |
| 195 | + | |
| 196 | + $badgeShape = Arr::get($settings, 'soldOutBadgeShape', 'badge'); | |
| 197 | + if (!in_array($badgeShape, ['badge', 'ribbon'], true)) { | |
| 198 | + $badgeShape = 'badge'; | |
| 199 | + } | |
| 200 | + | |
| 201 | + $badgePosition = Arr::get($settings, 'soldOutBadgePosition', 'top-left'); | |
| 202 | + if (!in_array($badgePosition, ['top-left', 'top-right', 'bottom-left', 'bottom-right'], true)) { | |
| 203 | + $badgePosition = 'top-left'; | |
| 204 | + } | |
| 205 | + | |
| 206 | + $classes = 'fct-sold-out-badge fct-sold-out-badge--' . $badgeShape . ' fct-sold-out-badge--' . $badgePosition; | |
| 207 | + | |
| 208 | + return '<span class="' . esc_attr($classes) . '">' . esc_html($badgeText) . '</span>'; | |
| 209 | + } | |
| 210 | + | |
| 211 | + public static function getAllowedHtmlForContent() | |
| 212 | + { | |
| 213 | + $allowed_html = wp_kses_allowed_html('post'); | |
| 214 | + $allowed_html['iframe'] = [ | |
| 215 | + 'src' => true, | |
| 216 | + 'width' => true, | |
| 217 | + 'height' => true, | |
| 218 | + 'frameborder' => true, | |
| 219 | + 'allow' => true, | |
| 220 | + 'allowfullscreen' => true, | |
| 221 | + 'title' => true, | |
| 222 | + 'referrerpolicy' => true, | |
| 223 | + ]; | |
| 224 | + return $allowed_html; | |
| 64 | 225 | } |
| 65 | 226 | } |