| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\Renderer; |
| 4 |
|
| 5 |
use FluentCart\App\App; |
| 6 |
use FluentCart\App\Services\FrontendView; |
| 7 |
use FluentCart\App\Vite; |
| 8 |
use FluentCart\Api\StoreSettings; |
| 9 |
use FluentCart\Api\ModuleSettings; |
| 10 |
use FluentCart\App\Helpers\Helper; |
| 11 |
use FluentCart\App\Models\Product; |
| 12 |
use FluentCart\Framework\Support\Arr; |
| 13 |
use FluentCart\App\Http\Routes\WebRoutes; |
| 14 |
use FluentCart\App\Models\ProductVariation; |
| 15 |
use FluentCart\Framework\Support\Collection; |
| 16 |
use FluentCart\App\Modules\Templating\AssetLoader; |
| 17 |
|
| 18 |
class ProductRenderer |
| 19 |
{ |
| 20 |
protected $product; |
| 21 |
|
| 22 |
protected $variants; |
| 23 |
|
| 24 |
protected $storeSettings; |
| 25 |
|
| 26 |
protected $defaultVariant = null; |
| 27 |
|
| 28 |
protected $hasOnetime = false; |
| 29 |
|
| 30 |
protected $hasSubscription = false; |
| 31 |
|
| 32 |
protected $viewType = ''; |
| 33 |
|
| 34 |
protected $columnType = ''; |
| 35 |
|
| 36 |
protected $defaultVariationId = ''; |
| 37 |
|
| 38 |
protected $defaultGalleryImageId = 0; |
| 39 |
|
| 40 |
protected $galleryActiveSet = false; |
| 41 |
|
| 42 |
protected $paymentTypes = []; |
| 43 |
|
| 44 |
protected $variantsByPaymentTypes = []; |
| 45 |
|
| 46 |
protected $activeTab = 'onetime'; |
| 47 |
|
| 48 |
protected $images = []; |
| 49 |
|
| 50 |
protected $defaultImageUrl = null; |
| 51 |
|
| 52 |
protected $defaultImageAlt = null; |
| 53 |
|
| 54 |
public function __construct(Product $product, $config = []) |
| 55 |
{ |
| 56 |
|
| 57 |
$this->product = $product; |
| 58 |
$this->variants = $product->variants; |
| 59 |
|
| 60 |
$this->storeSettings = new StoreSettings(); |
| 61 |
$this->viewType = $this->storeSettings->get('variation_view', 'both'); |
| 62 |
$this->columnType = $this->storeSettings->get('variation_columns', 'masonry'); |
| 63 |
|
| 64 |
$defaultVariationId = $config['default_variation_id'] ?? ''; |
| 65 |
|
| 66 |
// 'image', 'text','both' |
| 67 |
$this->viewType = apply_filters('fluent_cart/single_product/variation_view_type', $this->viewType, [ |
| 68 |
'product' => $product, |
| 69 |
'variants' => $this->variants, |
| 70 |
'defaultVariationId' => $defaultVariationId, |
| 71 |
]); |
| 72 |
|
| 73 |
// 'one', 'two','three', 'four', 'masonry' |
| 74 |
$this->columnType = apply_filters('fluent_cart/single_product/variation_column_type', $this->columnType, [ |
| 75 |
'product' => $product, |
| 76 |
'variants' => $this->variants, |
| 77 |
'defaultVariationId' => $defaultVariationId, |
| 78 |
]); |
| 79 |
|
| 80 |
|
| 81 |
$hasExplicitDefault = true; |
| 82 |
|
| 83 |
if (!$defaultVariationId) { |
| 84 |
$variationIds = $product->variants->pluck('id')->toArray(); |
| 85 |
$defaultVariationId = $product->detail->default_variation_id; |
| 86 |
|
| 87 |
if (!$defaultVariationId || !in_array($defaultVariationId, $variationIds)) { |
| 88 |
$defaultVariationId = Arr::get($variationIds, '0'); |
| 89 |
$hasExplicitDefault = false; |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
// Always set resolved default variation id |
| 94 |
$this->defaultVariationId = $defaultVariationId; |
| 95 |
|
| 96 |
// Gallery defaults to featured image (key 0) when no explicit default variation is set |
| 97 |
$this->defaultGalleryImageId = $hasExplicitDefault ? $defaultVariationId : 0; |
| 98 |
|
| 99 |
|
| 100 |
$this->product->variants->load('bundleChildren.product'); |
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
foreach ($this->product->variants as $variant) { |
| 105 |
if ($variant->id == $this->defaultVariationId) { |
| 106 |
$this->defaultVariant = $variant; |
| 107 |
} |
| 108 |
$paymentType = Arr::get($variant->other_info, 'payment_type'); |
| 109 |
if ($paymentType === 'onetime') { |
| 110 |
$this->hasOnetime = true; |
| 111 |
} else if ($paymentType === 'subscription') { |
| 112 |
$this->hasSubscription = true; |
| 113 |
} |
| 114 |
} |
| 115 |
|
| 116 |
$this->buildProductGroups(); |
| 117 |
} |
| 118 |
|
| 119 |
public function buildProductGroups() |
| 120 |
{ |
| 121 |
$groupKey = 'repeat_interval'; |
| 122 |
$otherInfo = (array)Arr::get($this->product->detail, 'other_info'); |
| 123 |
$groupBy = Arr::get($otherInfo, 'group_pricing_by', 'repeat_interval'); //repeat_interval,payment_type,none |
| 124 |
|
| 125 |
|
| 126 |
if ($groupBy !== 'none') { |
| 127 |
if ($groupBy === 'payment_type') { |
| 128 |
$groupKey = 'payment_type'; |
| 129 |
} |
| 130 |
|
| 131 |
$paymentTypes = []; |
| 132 |
|
| 133 |
if ($groupBy === 'repeat_interval') { |
| 134 |
foreach ($this->variants as $key => $variant) { |
| 135 |
$paymentType = 'onetime'; |
| 136 |
$type = Arr::get($variant, 'payment_type'); |
| 137 |
if ($type === 'subscription') { |
| 138 |
$isInstallment = Arr::get($variant, 'other_info.installment', 'no'); |
| 139 |
if ($isInstallment === 'yes' && App::isProActive()) { |
| 140 |
$paymentType = 'installment'; |
| 141 |
} else { |
| 142 |
$paymentType = Arr::get($variant, 'other_info.repeat_interval', 'onetime');; |
| 143 |
} |
| 144 |
} |
| 145 |
|
| 146 |
$paymentTypes[] = $paymentType; |
| 147 |
|
| 148 |
if (!isset($this->variantsByPaymentTypes[$paymentType])) { |
| 149 |
$this->variantsByPaymentTypes[$paymentType] = []; |
| 150 |
} |
| 151 |
|
| 152 |
$this->variantsByPaymentTypes[$paymentType][] = $variant; |
| 153 |
|
| 154 |
if ($this->defaultVariationId == $variant['id']) { |
| 155 |
$this->activeTab = $paymentType; |
| 156 |
} |
| 157 |
|
| 158 |
} |
| 159 |
} else { |
| 160 |
foreach ($this->variants as $key => $variant) { |
| 161 |
$paymentType = 'onetime'; |
| 162 |
$type = Arr::get($variant, 'payment_type'); |
| 163 |
if ($type === 'subscription') { |
| 164 |
$isInstallment = Arr::get($variant, 'other_info.installment'); |
| 165 |
if ($isInstallment === 'yes' && App::isProActive()) { |
| 166 |
$paymentType = 'installment'; |
| 167 |
} else { |
| 168 |
$paymentType = 'subscription'; |
| 169 |
} |
| 170 |
} |
| 171 |
$paymentTypes[] = $paymentType; |
| 172 |
|
| 173 |
if (!isset($this->variantsByPaymentTypes[$paymentType])) { |
| 174 |
$this->variantsByPaymentTypes[$paymentType] = []; |
| 175 |
} |
| 176 |
|
| 177 |
$this->variantsByPaymentTypes[$paymentType][] = $variant; |
| 178 |
|
| 179 |
if ($this->defaultVariationId == $variant['id']) { |
| 180 |
$this->activeTab = $paymentType; |
| 181 |
} |
| 182 |
|
| 183 |
} |
| 184 |
} |
| 185 |
|
| 186 |
$paymentTypes = array_unique($paymentTypes); |
| 187 |
|
| 188 |
|
| 189 |
$intervalOptions = Helper::getAvailableSubscriptionIntervalOptions(); |
| 190 |
|
| 191 |
$groupLanguageMap = [ |
| 192 |
'onetime' => __('One Time', 'fluent-cart'), |
| 193 |
'subscription' => __('Subscription', 'fluent-cart'), |
| 194 |
'installment' => __('Installment', 'fluent-cart'), |
| 195 |
]; |
| 196 |
|
| 197 |
foreach ($intervalOptions as $interval) { |
| 198 |
$groupLanguageMap[$interval['value']] = $interval['label']; |
| 199 |
} |
| 200 |
|
| 201 |
foreach ($paymentTypes as $paymentType) { |
| 202 |
$this->paymentTypes[$paymentType ?: 'onetime'] = Arr::get($groupLanguageMap, $paymentType ?: 'onetime'); |
| 203 |
} |
| 204 |
} |
| 205 |
} |
| 206 |
|
| 207 |
public function render() |
| 208 |
{ |
| 209 |
?> |
| 210 |
<div class="fct-single-product-page" data-fluent-cart-single-product-page data-product-id="<?php echo esc_attr($this->product->ID); ?>"> |
| 211 |
<div class="fct-single-product-page-row"> |
| 212 |
<?php $this->renderGallery(); ?> |
| 213 |
<div class="fct-product-summary"> |
| 214 |
<?php |
| 215 |
$this->renderTitle(); |
| 216 |
$this->renderProductMeta(); |
| 217 |
$this->renderExcerpt(); |
| 218 |
$this->renderPrices(); |
| 219 |
|
| 220 |
if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) { |
| 221 |
foreach ($this->product->variants as $variant) { |
| 222 |
$this->renderVariationsBundleProduct($variant); |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
$this->renderPackageDescription(); |
| 227 |
$this->renderBuySection(); |
| 228 |
?> |
| 229 |
</div> |
| 230 |
</div> |
| 231 |
</div> |
| 232 |
<?php |
| 233 |
} |
| 234 |
|
| 235 |
public function renderProductMeta() { |
| 236 |
?> |
| 237 |
<div class="fct-product-meta"> |
| 238 |
<?php $this->renderStockAvailability(); ?> |
| 239 |
<?php $this->renderSku(); ?> |
| 240 |
</div> |
| 241 |
|
| 242 |
<?php |
| 243 |
} |
| 244 |
|
| 245 |
public function renderBuySectionWrapperStart() |
| 246 |
{ |
| 247 |
?> |
| 248 |
<div aria-labelledby="fct-product-summary-title" data-fluent-cart-product-pricing-section data-product-id="<?php echo esc_attr($this->product->ID); ?>" class="fct_buy_section"> |
| 249 |
<?php |
| 250 |
} |
| 251 |
|
| 252 |
public function renderBuySectionWrapperEnd() |
| 253 |
{ |
| 254 |
?> |
| 255 |
</div> |
| 256 |
<?php |
| 257 |
} |
| 258 |
|
| 259 |
public function renderBuySection($atts = []) |
| 260 |
{ |
| 261 |
$otherInfo = (array)Arr::get($this->product->detail, 'other_info'); |
| 262 |
$groupBy = Arr::get($otherInfo, 'group_pricing_by', 'repeat_interval'); //repeat_interval,payment_type,none |
| 263 |
|
| 264 |
$this->renderBuySectionWrapperStart(); |
| 265 |
|
| 266 |
$this->renderVariationDisplay($atts); |
| 267 |
|
| 268 |
$this->renderItemPrice(); |
| 269 |
|
| 270 |
$this->renderQuantity(); |
| 271 |
?> |
| 272 |
<div class="fct-product-buttons-wrap"> |
| 273 |
<?php $this->renderPurchaseButtons(Arr::get($atts, 'button_atts', [])); ?> |
| 274 |
</div> |
| 275 |
<?php |
| 276 |
$this->renderBuySectionWrapperEnd(); |
| 277 |
} |
| 278 |
|
| 279 |
public function renderVariationDisplay($atts = []) |
| 280 |
{ |
| 281 |
$otherInfo = (array)Arr::get($this->product->detail, 'other_info'); |
| 282 |
$groupBy = Arr::get($otherInfo, 'group_pricing_by', 'repeat_interval'); //repeat_interval,payment_type,none |
| 283 |
|
| 284 |
if (count($this->paymentTypes) === 1 || $groupBy === 'none') { |
| 285 |
$this->renderVariants(Arr::get($atts, 'variation_atts', [])); |
| 286 |
} else { |
| 287 |
$this->renderTab(Arr::get($atts, 'variation_atts', [])); |
| 288 |
} |
| 289 |
} |
| 290 |
|
| 291 |
public function renderGalleryThumb() |
| 292 |
{ |
| 293 |
$thumbnails = []; |
| 294 |
|
| 295 |
$featuredMedia = $this->product->thumbnail ?? Vite::getAssetUrl('images/placeholder.svg'); |
| 296 |
|
| 297 |
if (!$featuredMedia) { |
| 298 |
$featuredMedia = []; |
| 299 |
} |
| 300 |
|
| 301 |
$galleryImage = get_post_meta($this->product->ID, 'fluent-products-gallery-image', true); |
| 302 |
|
| 303 |
if (!empty($galleryImage)) { |
| 304 |
$thumbnails[0] = [ |
| 305 |
'media' => $galleryImage, |
| 306 |
]; |
| 307 |
} |
| 308 |
|
| 309 |
foreach ($this->variants as $variant) { |
| 310 |
if (!empty($variant['media']['meta_value'])) { |
| 311 |
$thumbnails[$variant['id']] = [ |
| 312 |
'media' => $variant['media']['meta_value'], |
| 313 |
]; |
| 314 |
} else { |
| 315 |
$this->defaultImageUrl = $featuredMedia; |
| 316 |
$this->defaultImageAlt = Arr::get($variant, 'variation_title', ''); |
| 317 |
} |
| 318 |
} |
| 319 |
|
| 320 |
$images = empty($thumbnails) ? [] : $thumbnails; |
| 321 |
|
| 322 |
|
| 323 |
|
| 324 |
$this->images = $images; |
| 325 |
|
| 326 |
if (!empty($images)) { |
| 327 |
$imageId = $this->defaultGalleryImageId; |
| 328 |
|
| 329 |
if (isset($images[$imageId])) { |
| 330 |
$imageMetaValue = $images[$imageId]; |
| 331 |
$this->defaultImageUrl = Arr::get($imageMetaValue, 'media.0.url', ''); |
| 332 |
$this->defaultImageAlt = Arr::get($imageMetaValue, 'media.0.title', ''); |
| 333 |
} |
| 334 |
} |
| 335 |
|
| 336 |
?> |
| 337 |
<div class="fct-product-gallery-thumb" role="region" |
| 338 |
aria-label="<?php echo esc_attr($this->product->post_title . ' gallery'); ?>"> |
| 339 |
<img |
| 340 |
src="<?php echo esc_url($this->defaultImageUrl ?? '') ?>" |
| 341 |
alt="<?php echo esc_attr($this->defaultImageAlt) ?>" |
| 342 |
data-fluent-cart-single-product-page-product-thumbnail |
| 343 |
data-default-image-url="<?php echo esc_url($featuredMedia) ?>" |
| 344 |
/> |
| 345 |
</div> |
| 346 |
<?php |
| 347 |
} |
| 348 |
|
| 349 |
public function renderGalleryThumbControls($maxThumbnails = null) |
| 350 |
{ |
| 351 |
$totalThumbImages = Arr::pluck($this->images, 'media.*.url'); |
| 352 |
|
| 353 |
if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1){ |
| 354 |
|
| 355 |
return ''; |
| 356 |
} |
| 357 |
|
| 358 |
// Collect ALL gallery images as JSON for lightbox (even when max thumbnails limits visible thumbs) |
| 359 |
$allGalleryImages = []; |
| 360 |
foreach ($this->images as $imageId => $image) { |
| 361 |
if (empty($image['media']) || !is_array($image['media'])) { |
| 362 |
continue; |
| 363 |
} |
| 364 |
foreach ($image['media'] as $item) { |
| 365 |
$url = Arr::get($item, 'url', ''); |
| 366 |
if (empty($url)) { |
| 367 |
continue; |
| 368 |
} |
| 369 |
$allGalleryImages[] = [ |
| 370 |
'url' => $url, |
| 371 |
'title' => Arr::get($item, 'title', ''), |
| 372 |
'variation_id' => (string) $imageId, |
| 373 |
]; |
| 374 |
} |
| 375 |
} |
| 376 |
|
| 377 |
?> |
| 378 |
|
| 379 |
<div class="fct-gallery-thumb-controls" |
| 380 |
role="toolbar" |
| 381 |
aria-label="<?php echo esc_attr__('Product image thumbnails', 'fluent-cart'); ?>" |
| 382 |
data-fluent-cart-single-product-page-product-thumbnail-controls |
| 383 |
data-all-gallery-images="<?php echo esc_attr(wp_json_encode($allGalleryImages) ?: '[]'); ?>"> |
| 384 |
|
| 385 |
<?php $this->renderGalleryThumbControl($maxThumbnails); ?> |
| 386 |
|
| 387 |
</div> |
| 388 |
|
| 389 |
<?php |
| 390 |
|
| 391 |
} |
| 392 |
|
| 393 |
public function renderGalleryThumbControl($maxThumbnails = null) |
| 394 |
{ |
| 395 |
if ($maxThumbnails !== null && $maxThumbnails <= 0) { |
| 396 |
$maxThumbnails = null; // treat invalid value as "no limit" |
| 397 |
} |
| 398 |
|
| 399 |
$count = 0; |
| 400 |
$totalImages = 0; |
| 401 |
|
| 402 |
// First, count total renderable images |
| 403 |
foreach ($this->images as $imageId => $image) { |
| 404 |
if (empty($image['media']) || !is_array($image['media'])) { |
| 405 |
continue; |
| 406 |
} |
| 407 |
foreach ($image['media'] as $item) { |
| 408 |
if (!empty(Arr::get($item, 'url', ''))) { |
| 409 |
$totalImages++; |
| 410 |
} |
| 411 |
} |
| 412 |
} |
| 413 |
|
| 414 |
// Then render up to max |
| 415 |
foreach ($this->images as $imageId => $image) { |
| 416 |
if (empty($image['media']) || !is_array($image['media'])) { |
| 417 |
continue; |
| 418 |
} |
| 419 |
|
| 420 |
foreach ($image['media'] as $item) { |
| 421 |
if (empty(Arr::get($item, 'url', ''))) { |
| 422 |
continue; |
| 423 |
} |
| 424 |
|
| 425 |
if ($maxThumbnails !== null && $count >= (int) $maxThumbnails) { |
| 426 |
$this->renderGallerySeeMoreButton($totalImages - (int) $maxThumbnails); |
| 427 |
return; |
| 428 |
} |
| 429 |
|
| 430 |
$this->renderGalleryThumbControlButton($item, $imageId); |
| 431 |
$count++; |
| 432 |
} |
| 433 |
|
| 434 |
} |
| 435 |
|
| 436 |
} |
| 437 |
|
| 438 |
public function renderGallerySeeMoreButton($remainingCount) |
| 439 |
{ |
| 440 |
?> |
| 441 |
<button |
| 442 |
type="button" |
| 443 |
class="fct-gallery-see-more-button" |
| 444 |
data-fluent-cart-gallery-see-more |
| 445 |
aria-label="<?php echo esc_attr( |
| 446 |
sprintf( |
| 447 |
/* translators: %d number of remaining images */ |
| 448 |
__('View all %d more images', 'fluent-cart'), |
| 449 |
$remainingCount |
| 450 |
) |
| 451 |
); ?>" |
| 452 |
> |
| 453 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 454 |
<path d="M2.58078 19.0103L2.56078 19.0303C2.29078 18.4403 2.12078 17.7703 2.05078 17.0303C2.12078 17.7603 2.31078 18.4203 2.58078 19.0103Z" fill="#9D9FAC"/> |
| 455 |
<path d="M8.99914 10.3801C10.3136 10.3801 11.3791 9.31456 11.3791 8.00012C11.3791 6.68568 10.3136 5.62012 8.99914 5.62012C7.6847 5.62012 6.61914 6.68568 6.61914 8.00012C6.61914 9.31456 7.6847 10.3801 8.99914 10.3801Z" fill="#9D9FAC"/> |
| 456 |
<path d="M16.19 2H7.81C4.17 2 2 4.17 2 7.81V16.19C2 17.28 2.19 18.23 2.56 19.03C3.42 20.93 5.26 22 7.81 22H16.19C19.83 22 22 19.83 22 16.19V13.9V7.81C22 4.17 19.83 2 16.19 2ZM20.37 12.5C19.59 11.83 18.33 11.83 17.55 12.5L13.39 16.07C12.61 16.74 11.35 16.74 10.57 16.07L10.23 15.79C9.52 15.17 8.39 15.11 7.59 15.65L3.85 18.16C3.63 17.6 3.5 16.95 3.5 16.19V7.81C3.5 4.99 4.99 3.5 7.81 3.5H16.19C19.01 3.5 20.5 4.99 20.5 7.81V12.61L20.37 12.5Z" fill="#9D9FAC"/> |
| 457 |
<script xmlns=""/></svg> |
| 458 |
|
| 459 |
<span class="fct-see-more-text"> |
| 460 |
<?php echo esc_html__('See', 'fluent-cart'); ?> |
| 461 |
<span class="fct-see-more-count"><?php echo esc_html($remainingCount); ?></span> |
| 462 |
<?php echo esc_html__('More', 'fluent-cart'); ?> |
| 463 |
</span> |
| 464 |
</button> |
| 465 |
<?php |
| 466 |
} |
| 467 |
|
| 468 |
public function renderGalleryThumbControlButton($item, $imageId) |
| 469 |
{ |
| 470 |
|
| 471 |
$isHidden = ''; //$imageId != $this->defaultVariationId ? 'is-hidden' : ''; |
| 472 |
$itemUrl = Arr::get($item, 'url', ''); |
| 473 |
$itemTitle = Arr::get($item, 'title', ''); |
| 474 |
$isSelected = !$this->galleryActiveSet && $imageId == $this->defaultGalleryImageId; |
| 475 |
if ($isSelected) { |
| 476 |
$this->galleryActiveSet = true; |
| 477 |
} |
| 478 |
?> |
| 479 |
|
| 480 |
<button |
| 481 |
type="button" |
| 482 |
class="fct-gallery-thumb-control-button <?php echo $isSelected ? 'active' : ''; ?> <?php echo esc_attr($isHidden); ?>" |
| 483 |
data-fluent-cart-thumb-control-button |
| 484 |
data-url="<?php echo esc_url($itemUrl); ?>" |
| 485 |
data-variation-id="<?php echo esc_attr($imageId); ?>" |
| 486 |
aria-label="<?php echo |
| 487 |
/* translators: %s image title */ |
| 488 |
esc_attr(sprintf(__('View %s image', 'fluent-cart'), $itemTitle)); |
| 489 |
?>" |
| 490 |
aria-pressed="<?php echo $isSelected ? 'true' : 'false'; ?>" |
| 491 |
tabindex="<?php echo $isSelected ? '0' : '-1'; ?>" |
| 492 |
> |
| 493 |
<img |
| 494 |
class="fct-gallery-control-thumb" |
| 495 |
data-fluent-cart-single-product-page-product-thumbnail-controls-thumb |
| 496 |
src="<?php echo esc_url($itemUrl); ?>" |
| 497 |
alt="<?php echo esc_attr($itemTitle); ?>" |
| 498 |
/> |
| 499 |
</button> |
| 500 |
|
| 501 |
<?php |
| 502 |
|
| 503 |
|
| 504 |
} |
| 505 |
|
| 506 |
public function renderGallery($args = []) |
| 507 |
{ |
| 508 |
|
| 509 |
$defaults = [ |
| 510 |
'thumbnail_mode' => 'all', // horizontal, vertical |
| 511 |
'thumb_position' => 'bottom', // bottom, left, right, top |
| 512 |
'scrollable_thumbs' => 'no', // yes / no |
| 513 |
'max_thumbnails' => null, // null = no limit, integer = max visible |
| 514 |
]; |
| 515 |
|
| 516 |
$atts = wp_parse_args($args, $defaults); |
| 517 |
|
| 518 |
$thumbnailMode = $atts['thumbnail_mode']; |
| 519 |
|
| 520 |
$wrapperAtts = [ |
| 521 |
'class' => 'fct-product-gallery-wrapper ' . 'thumb-pos-' . $atts['thumb_position'] . ' thumb-mode-' . $thumbnailMode, |
| 522 |
'data-fct-product-gallery' => '', |
| 523 |
'data-fluent-cart-product-gallery-wrapper' => '', |
| 524 |
'data-thumbnail-mode' => $thumbnailMode, |
| 525 |
'data-product-id' => $this->product->ID, |
| 526 |
'data-scrollable-thumbs' => $atts['scrollable_thumbs'], |
| 527 |
]; |
| 528 |
|
| 529 |
?> |
| 530 |
|
| 531 |
<div <?php RenderHelper::renderAtts($wrapperAtts); ?>> |
| 532 |
|
| 533 |
<?php |
| 534 |
$this->renderGalleryThumb(); |
| 535 |
$this->renderGalleryThumbControls($atts['max_thumbnails']); |
| 536 |
?> |
| 537 |
</div> |
| 538 |
|
| 539 |
<?php |
| 540 |
} |
| 541 |
|
| 542 |
public function renderTitle() |
| 543 |
{ |
| 544 |
?> |
| 545 |
<div class="fct-product-title"> |
| 546 |
<h1 id="fct-product-summary-title"><?php echo esc_html($this->product->post_title); ?></h1> |
| 547 |
</div> |
| 548 |
<?php |
| 549 |
} |
| 550 |
|
| 551 |
public function renderStockAvailability($wrapper_attributes = '') |
| 552 |
{ |
| 553 |
if (!ModuleSettings::isActive('stock_management')) { |
| 554 |
return ''; |
| 555 |
} |
| 556 |
|
| 557 |
$stockAvailability = $this->product->detail->getStockAvailability(); |
| 558 |
|
| 559 |
if (!Arr::get($stockAvailability, 'manage_stock')) { |
| 560 |
return ''; |
| 561 |
} |
| 562 |
|
| 563 |
$isStock = $this->product->isStock(); |
| 564 |
|
| 565 |
// Check default variant stock for both simple and variable products |
| 566 |
if ($this->defaultVariant) { |
| 567 |
$isStock = $isStock && $this->defaultVariant->isStock(); |
| 568 |
} |
| 569 |
$stockLabel = $isStock ? $stockAvailability['availability'] : __('Out of Stock', 'fluent-cart'); |
| 570 |
$statusClass = $isStock ? ($stockAvailability['class'] ?? '') : 'out-of-stock'; |
| 571 |
echo sprintf( |
| 572 |
'<div class="fct-product-stock %1$s" role="status" aria-live="polite"> |
| 573 |
<div %2$s> |
| 574 |
<span class="fct-stock-label">%3$s</span> |
| 575 |
<span class="fct-stock-badge fct_status_badge_%1$s" data-fluent-cart-product-stock> |
| 576 |
%4$s |
| 577 |
</span> |
| 578 |
</div> |
| 579 |
</div>', |
| 580 |
esc_attr($statusClass), |
| 581 |
$wrapper_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 582 |
esc_html__('Availability:', 'fluent-cart'), |
| 583 |
esc_html($stockLabel) |
| 584 |
); |
| 585 |
} |
| 586 |
|
| 587 |
public function renderSku($wrapper_attributes = '', $showLabel = true, $label = '', $variant = null) |
| 588 |
{ |
| 589 |
if (!$variant) { |
| 590 |
$variant = $this->defaultVariant ?: $this->product->variants->first(); |
| 591 |
} |
| 592 |
|
| 593 |
if (!$variant || empty($variant->sku)) { |
| 594 |
return; |
| 595 |
} |
| 596 |
|
| 597 |
if (!$label) { |
| 598 |
$label = __('SKU:', 'fluent-cart'); |
| 599 |
} |
| 600 |
|
| 601 |
$labelHtml = ''; |
| 602 |
if ($showLabel && $label) { |
| 603 |
$labelHtml = sprintf('<span class="fct-product-sku__label">%s</span> ', esc_html($label)); |
| 604 |
} |
| 605 |
|
| 606 |
echo sprintf( |
| 607 |
'<div class="fct-product-sku"> |
| 608 |
<div %s> |
| 609 |
%s<span class="fct-product-sku__value" data-fluent-cart-product-sku>%s</span> |
| 610 |
</div> |
| 611 |
</div>', |
| 612 |
$wrapper_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 613 |
$labelHtml, |
| 614 |
esc_html($variant->sku) |
| 615 |
); |
| 616 |
} |
| 617 |
|
| 618 |
public function renderPackageDescription($wrapper_attributes = '', $showName = true, $showDimensions = true, $showProductWeight = true, $showTotalWeight = true, $variant = null) |
| 619 |
{ |
| 620 |
$variant = $variant ?: ($this->defaultVariant ?: $this->product->variants->first()); |
| 621 |
(new ProductCardRender($this->product))->renderPackageDescription($wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $variant); |
| 622 |
} |
| 623 |
|
| 624 |
/** |
| 625 |
* Build a JSON string of package info for a variant (used as data attribute for JS switching). |
| 626 |
*/ |
| 627 |
private function getVariantPackageInfoJson(ProductVariation $variant) |
| 628 |
{ |
| 629 |
$otherInfo = $variant->other_info ?: []; |
| 630 |
$packageSlug = Arr::get($otherInfo, 'package_slug', ''); |
| 631 |
$package = Helper::getPackageBySlug($packageSlug); |
| 632 |
|
| 633 |
if (!$package) { |
| 634 |
return ''; |
| 635 |
} |
| 636 |
|
| 637 |
static $storeWeightUnit = null; |
| 638 |
|
| 639 |
if ($storeWeightUnit === null) { |
| 640 |
$storeWeightUnit = Helper::shopConfig('weight_unit') ?: 'kg'; |
| 641 |
} |
| 642 |
|
| 643 |
// Format dimensions |
| 644 |
$length = Arr::get($package, 'length', ''); |
| 645 |
$width = Arr::get($package, 'width', ''); |
| 646 |
$height = Arr::get($package, 'height', ''); |
| 647 |
$dimensionUnit = Arr::get($package, 'dimension_unit', 'cm'); |
| 648 |
$dimensionParts = array_filter([$length, $width, $height], function ($val) { |
| 649 |
return $val !== '' && $val !== null && $val != 0; |
| 650 |
}); |
| 651 |
$formattedDimensions = $dimensionParts |
| 652 |
? implode(' × ', $dimensionParts) . ' ' . $dimensionUnit |
| 653 |
: ''; |
| 654 |
|
| 655 |
// Calculate weights |
| 656 |
$productWeight = floatval(Arr::get($otherInfo, 'weight', 0)); |
| 657 |
$productWeightUnit = Arr::get($otherInfo, 'weight_unit', $storeWeightUnit); |
| 658 |
$convertedProductWeight = Helper::convertWeight($productWeight, $productWeightUnit, $storeWeightUnit); |
| 659 |
|
| 660 |
$packageWeight = floatval(Arr::get($package, 'weight', 0)); |
| 661 |
$packageWeightUnit = Arr::get($package, 'weight_unit', $storeWeightUnit); |
| 662 |
$convertedPackageWeight = Helper::convertWeight($packageWeight, $packageWeightUnit, $storeWeightUnit); |
| 663 |
$totalWeight = $convertedProductWeight + $convertedPackageWeight; |
| 664 |
|
| 665 |
// Format weights |
| 666 |
$formattedProductWeight = $convertedProductWeight |
| 667 |
? rtrim(rtrim(number_format($convertedProductWeight, 2), '0'), '.') . ' ' . $storeWeightUnit |
| 668 |
: ''; |
| 669 |
|
| 670 |
$formattedShippingWeight = ($totalWeight && $convertedPackageWeight) |
| 671 |
? rtrim(rtrim(number_format($totalWeight, 2), '0'), '.') . ' ' . $storeWeightUnit |
| 672 |
: ''; |
| 673 |
|
| 674 |
return wp_json_encode([ |
| 675 |
'name' => Arr::get($package, 'name', ''), |
| 676 |
'dimensions' => $formattedDimensions, |
| 677 |
'product_weight' => $formattedProductWeight, |
| 678 |
'shipping_weight' => $formattedShippingWeight, |
| 679 |
]); |
| 680 |
} |
| 681 |
|
| 682 |
public function renderExcerpt() |
| 683 |
{ |
| 684 |
$excerpt = $this->product->post_excerpt; |
| 685 |
if (!$excerpt) { |
| 686 |
return; |
| 687 |
} |
| 688 |
?> |
| 689 |
<div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title"> |
| 690 |
<p><?php echo wp_kses_post($excerpt); ?></p> |
| 691 |
</div> |
| 692 |
<?php |
| 693 |
|
| 694 |
} |
| 695 |
|
| 696 |
public function renderDescription() |
| 697 |
{ |
| 698 |
$post = get_post($this->product->ID); |
| 699 |
if (!$post || empty($post->post_content)) { |
| 700 |
return; |
| 701 |
} |
| 702 |
?> |
| 703 |
<div class="fct-product-description"> |
| 704 |
<?php echo apply_filters('the_content', $post->post_content); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
| 705 |
</div> |
| 706 |
<?php |
| 707 |
} |
| 708 |
|
| 709 |
public function renderPrices() |
| 710 |
{ |
| 711 |
if ($this->product->detail->variation_type === 'simple') { |
| 712 |
// we have to render for the simple product |
| 713 |
|
| 714 |
$first_price = $this->product->variants()->first(); |
| 715 |
|
| 716 |
$itemPrice = $first_price ? $first_price->item_price : 0; |
| 717 |
$itemPrice = apply_filters('fluent_cart/product/display_price', $itemPrice, [ |
| 718 |
'product' => $this->product, |
| 719 |
'variation' => $first_price, |
| 720 |
]); |
| 721 |
$itemPrice = (int)$itemPrice; |
| 722 |
$comparePrice = $first_price ? (int)$first_price->compare_price : 0; |
| 723 |
if ($comparePrice <= $itemPrice) { |
| 724 |
$comparePrice = 0; |
| 725 |
} |
| 726 |
do_action('fluent_cart/product/single/before_price_block', [ |
| 727 |
'product' => $this->product, |
| 728 |
'current_price' => $itemPrice, |
| 729 |
'scope' => 'price_range' |
| 730 |
]); |
| 731 |
?> |
| 732 |
<?php |
| 733 |
|
| 734 |
if ($comparePrice) { |
| 735 |
$aria_label = sprintf( |
| 736 |
/* translators: 1: Original price, 2: Current item price */ |
| 737 |
__('Original Price: %1$s, Price: %2$s', 'fluent-cart'), |
| 738 |
Helper::toDecimal($comparePrice), |
| 739 |
Helper::toDecimal($itemPrice) |
| 740 |
); |
| 741 |
} else { |
| 742 |
$aria_label = sprintf( |
| 743 |
/* translators: 1: Current item price */ |
| 744 |
__('Price: %1$s', 'fluent-cart'), |
| 745 |
Helper::toDecimal($itemPrice) |
| 746 |
); |
| 747 |
} |
| 748 |
|
| 749 |
?> |
| 750 |
<div class="fct-price-range fct-product-prices" role="term" |
| 751 |
aria-label="<?php echo esc_attr($aria_label); ?>"> |
| 752 |
|
| 753 |
<?php if ($comparePrice): ?> |
| 754 |
<span class="fct-compare-price"> |
| 755 |
<del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>"><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></del> |
| 756 |
</span> |
| 757 |
<?php endif; ?> |
| 758 |
<span class="fct-item-price" aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>"> |
| 759 |
<?php echo esc_html(Helper::toDecimal($itemPrice)); ?> |
| 760 |
<?php do_action('fluent_cart/product/after_price', [ |
| 761 |
'product' => $this->product, |
| 762 |
'current_price' => $itemPrice, |
| 763 |
'scope' => 'price_range' |
| 764 |
]); ?> |
| 765 |
</span> |
| 766 |
</div> |
| 767 |
<?php |
| 768 |
do_action('fluent_cart/product/single/after_price_block', [ |
| 769 |
'product' => $this->product, |
| 770 |
'current_price' => $itemPrice, |
| 771 |
'scope' => 'price_range' |
| 772 |
]); |
| 773 |
return; |
| 774 |
} |
| 775 |
$min_price = $this->product->detail->min_price; |
| 776 |
$max_price = $this->product->detail->max_price; |
| 777 |
|
| 778 |
do_action('fluent_cart/product/single/before_price_range_block', [ |
| 779 |
'product' => $this->product, |
| 780 |
'current_price' => $min_price, |
| 781 |
'scope' => 'price_range' |
| 782 |
]); |
| 783 |
?> |
| 784 |
<?php |
| 785 |
$aria_label = sprintf( |
| 786 |
/* translators: 1: Minimum price, 2: Maximum price */ |
| 787 |
__('Price range: %1$s - %2$s', 'fluent-cart'), |
| 788 |
Helper::toDecimal($min_price), |
| 789 |
Helper::toDecimal($max_price) |
| 790 |
); |
| 791 |
?> |
| 792 |
<div class="fct-product-prices fct-price-range" role="term" aria-label="<?php echo esc_attr($aria_label); ?>"> |
| 793 |
|
| 794 |
<?php if ($max_price && $max_price != $min_price && $max_price > $min_price): ?> |
| 795 |
<span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span> |
| 796 |
<span class="fct-price-separator" aria-hidden="true">-</span> |
| 797 |
<?php endif; ?> |
| 798 |
<span class="fct-max-price"> |
| 799 |
<?php echo esc_html(Helper::toDecimal($max_price)); ?> |
| 800 |
</span> |
| 801 |
|
| 802 |
<?php do_action('fluent_cart/product/after_price', [ |
| 803 |
'product' => $this->product, |
| 804 |
'current_price' => $min_price, |
| 805 |
'scope' => 'price_range' |
| 806 |
]); ?> |
| 807 |
|
| 808 |
</div> |
| 809 |
<?php |
| 810 |
do_action('fluent_cart/product/single/after_price_range_block', [ |
| 811 |
'product' => $this->product, |
| 812 |
'current_price' => $min_price, |
| 813 |
'scope' => 'price_range' |
| 814 |
]); |
| 815 |
} |
| 816 |
|
| 817 |
public function renderVariants($atts = []) |
| 818 |
{ |
| 819 |
if ($this->product->detail->variation_type === 'simple') { |
| 820 |
return; |
| 821 |
} |
| 822 |
|
| 823 |
$variants = $this->product->variants; |
| 824 |
if (!$variants || $variants->isEmpty()) { |
| 825 |
return; |
| 826 |
} |
| 827 |
|
| 828 |
// Sort by serial_index ascending |
| 829 |
$variants = $variants->sortBy('serial_index')->values(); |
| 830 |
|
| 831 |
$classes = array_filter([ |
| 832 |
'fct-product-variants', |
| 833 |
'column-type-' . $this->columnType, |
| 834 |
Arr::get($atts, 'wrapper_class', ''), |
| 835 |
]); |
| 836 |
|
| 837 |
?> |
| 838 |
<div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup" |
| 839 |
aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>"> |
| 840 |
<?php foreach ($variants as $variant) { |
| 841 |
do_action('fluent_cart/product/single/before_variant_item', [ |
| 842 |
'product' => $this->product, |
| 843 |
'variant' => $variant, |
| 844 |
'scope' => 'product_variant_item' |
| 845 |
]); |
| 846 |
$this->renderVariationItem($variant, $this->defaultVariationId); |
| 847 |
do_action('fluent_cart/product/single/after_variant_item', [ |
| 848 |
'product' => $this->product, |
| 849 |
'variant' => $variant, |
| 850 |
'scope' => 'product_variant_item' |
| 851 |
]); |
| 852 |
} ?> |
| 853 |
</div> |
| 854 |
<?php |
| 855 |
} |
| 856 |
|
| 857 |
public function renderItemPrice() |
| 858 |
{ |
| 859 |
if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) { |
| 860 |
return; // for simple product we already rendered the price |
| 861 |
} |
| 862 |
|
| 863 |
do_action('fluent_cart/product/single/before_price_block', [ |
| 864 |
'product' => $this->product, |
| 865 |
'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0, |
| 866 |
'scope' => 'product_variant_price' |
| 867 |
]); |
| 868 |
|
| 869 |
foreach ($this->product->variants as $variant) { |
| 870 |
if ($this->shouldRenderPriceInPriceSection()) { |
| 871 |
$this->renderVariantPricingWrapperStart($variant); |
| 872 |
$paymentType = Arr::get($variant->other_info, 'payment_type', 'onetime'); |
| 873 |
if (!$this->hasSubscription) { |
| 874 |
$this->renderVariationComparePrice($variant); |
| 875 |
$this->applyVariationPriceFilter($variant, $paymentType); |
| 876 |
} else { |
| 877 |
|
| 878 |
$atts = [ |
| 879 |
'class' => 'fct-product-payment-type fluent-cart-product-variation-content' . ($this->defaultVariant->id != $variant->id ? ' is-hidden' : ''), |
| 880 |
'data-fluent-cart-product-payment-type' => '', |
| 881 |
'data-variation-id' => $variant->id |
| 882 |
]; |
| 883 |
|
| 884 |
$this->renderComparePriceWrapperStart($atts); |
| 885 |
$this->renderVariationComparePrice($variant); |
| 886 |
if ($paymentType === 'onetime') { |
| 887 |
echo esc_html(Helper::toDecimal($variant->item_price)); |
| 888 |
} else { |
| 889 |
$this->applyVariationPriceFilter($variant, $paymentType); |
| 890 |
} |
| 891 |
$this->renderComparePriceWrapperEnd(); |
| 892 |
} |
| 893 |
|
| 894 |
$this->renderVariantPricingWrapperEnd(); |
| 895 |
} |
| 896 |
|
| 897 |
} |
| 898 |
|
| 899 |
|
| 900 |
foreach ($this->product->variants as $variant) { |
| 901 |
$this->renderVariationsBundleProduct($variant); |
| 902 |
} |
| 903 |
|
| 904 |
|
| 905 |
|
| 906 |
|
| 907 |
do_action('fluent_cart/product/single/after_price_block', [ |
| 908 |
'product' => $this->product, |
| 909 |
'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0, |
| 910 |
'scope' => 'product_variant_price' |
| 911 |
]); |
| 912 |
} |
| 913 |
|
| 914 |
public function shouldRenderPriceInPriceSection(): bool |
| 915 |
{ |
| 916 |
return !($this->viewType === 'text' && $this->columnType === 'one'); |
| 917 |
} |
| 918 |
|
| 919 |
public function applyVariationPriceFilter($variant, $paymentType = 'onetime') |
| 920 |
{ |
| 921 |
$priceText = $paymentType === 'onetime' ? Helper::toDecimal($variant->item_price) : $variant->getSubscriptionTermsText(true); |
| 922 |
echo wp_kses_post(apply_filters('fluent_cart/single_product/variation_price', esc_html($priceText), [ |
| 923 |
'product' => $this->product, |
| 924 |
'variant' => $variant, |
| 925 |
'scope' => 'product_variant_price' |
| 926 |
])); |
| 927 |
do_action('fluent_cart/product/after_price', [ |
| 928 |
'product' => $this->product, |
| 929 |
'current_price' => $variant->item_price, |
| 930 |
'scope' => 'product_variant_price' |
| 931 |
]); |
| 932 |
} |
| 933 |
|
| 934 |
public function renderComparePriceWrapperStart($atts = []) |
| 935 |
{ |
| 936 |
?> |
| 937 |
<div <?php $this->renderAttributes($atts); ?> > |
| 938 |
<?php |
| 939 |
} |
| 940 |
|
| 941 |
public function renderComparePriceWrapperEnd() |
| 942 |
{ |
| 943 |
?> |
| 944 |
</div> |
| 945 |
<?php |
| 946 |
} |
| 947 |
|
| 948 |
public function renderVariationComparePrice($variant) |
| 949 |
{ |
| 950 |
if (!$variant->compare_price) { |
| 951 |
return; |
| 952 |
} ?> |
| 953 |
|
| 954 |
<span class="fct-compare-price"> |
| 955 |
<del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del> |
| 956 |
</span> |
| 957 |
<?php |
| 958 |
} |
| 959 |
|
| 960 |
public function renderVariantPricingWrapperStart($variant) |
| 961 |
{ ?> |
| 962 |
<div |
| 963 |
class="fct-product-item-price fluent-cart-product-variation-content <?php echo esc_attr($this->defaultVariant->id != $variant->id ? ' is-hidden' : ''); ?>" |
| 964 |
data-fluent-cart-product-item-price |
| 965 |
data-variation-id="<?php echo esc_attr($variant->id); ?>" |
| 966 |
aria-live="polite" |
| 967 |
role="status" |
| 968 |
> |
| 969 |
<?php } |
| 970 |
|
| 971 |
|
| 972 |
public function renderVariantPricingWrapperEnd() |
| 973 |
{ |
| 974 |
?> </div> <?php |
| 975 |
} |
| 976 |
|
| 977 |
public function renderVariationsBundleProduct($variant) |
| 978 |
{ |
| 979 |
if (count($variant->bundleChildren) == 0) { |
| 980 |
return; |
| 981 |
} |
| 982 |
|
| 983 |
if(is_object($variant->bundleChildren)) { |
| 984 |
$bundleProducts = $variant->bundleChildren->toArray(); |
| 985 |
}else{ |
| 986 |
$bundleProducts = $variant->bundleChildren; |
| 987 |
} |
| 988 |
|
| 989 |
$total = count($bundleProducts); |
| 990 |
?> |
| 991 |
<div class="fluent-cart-product-variation-content fct-bundle-products <?php echo esc_attr($this->defaultVariant->id != $variant->id ? ' is-hidden' : ''); ?>" |
| 992 |
data-variation-id="<?php echo esc_attr($variant->id); ?>" |
| 993 |
data-fluent-cart-collapsibles |
| 994 |
> |
| 995 |
<h4 class="fct-bundle-products-title"> |
| 996 |
<?php echo esc_html__('Bundle of', 'fluent-cart') . ':'; ?> |
| 997 |
</h4> |
| 998 |
|
| 999 |
<div class="fct-bundle-products-list"> |
| 1000 |
<?php foreach (array_slice($bundleProducts, 0, 2) as $product): ?> |
| 1001 |
<p> |
| 1002 |
<?php echo esc_html(Arr::get($product, 'product.post_title')); ?> - |
| 1003 |
<?php echo esc_html($product['variation_title']); ?> |
| 1004 |
</p> |
| 1005 |
<?php endforeach; ?> |
| 1006 |
|
| 1007 |
<?php if($total > 2): ?> |
| 1008 |
<div class="fct-bundle-products-more"> |
| 1009 |
<div class="fct-bundle-products-more-list"> |
| 1010 |
<?php foreach (array_slice($bundleProducts, 2) as $product): ?> |
| 1011 |
<p> |
| 1012 |
<?php echo esc_html(Arr::get($product, 'product.post_title')); ?> - |
| 1013 |
<?php echo esc_html($product['variation_title']); ?> |
| 1014 |
</p> |
| 1015 |
<?php endforeach; ?> |
| 1016 |
</div> |
| 1017 |
</div> |
| 1018 |
<?php endif;?> |
| 1019 |
</div> |
| 1020 |
|
| 1021 |
<?php if ($total > 2) : ?> |
| 1022 |
<button type="button" class="fct-see-more-btn" data-fluent-cart-collapsible-toggle> |
| 1023 |
<span class="see-more-text"> |
| 1024 |
<?php echo esc_html__('See More', 'fluent-cart'); ?> |
| 1025 |
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7" fill="none"> |
| 1026 |
<path d="M0.75 0.75L5.04289 5.04289C5.37623 5.37623 5.54289 5.54289 5.75 5.54289C5.95711 5.54289 6.12377 5.37623 6.45711 5.04289L10.75 0.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 1027 |
</svg> |
| 1028 |
</span> |
| 1029 |
<span class="see-less-text"> |
| 1030 |
<?php echo esc_html__('See Less', 'fluent-cart'); ?> |
| 1031 |
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 14 8" fill="none"> |
| 1032 |
<path d="M0.75 6.54297L6.04289 1.25008C6.37623 0.916742 6.54289 0.750076 6.75 0.750076C6.95711 0.750076 7.12377 0.916742 7.45711 1.25008L12.75 6.54297" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 1033 |
</svg> |
| 1034 |
</span> |
| 1035 |
</button> |
| 1036 |
<?php endif; ?> |
| 1037 |
</div> |
| 1038 |
<?php } |
| 1039 |
|
| 1040 |
public function renderQuantity() |
| 1041 |
{ |
| 1042 |
$soldIndividually = $this->product->soldIndividually(); |
| 1043 |
|
| 1044 |
if (!$this->hasOnetime || $soldIndividually) { |
| 1045 |
return; |
| 1046 |
} |
| 1047 |
|
| 1048 |
$attributes = [ |
| 1049 |
'data-fluent-cart-product-quantity-container' => '', |
| 1050 |
'data-cart-id' => $this->defaultVariant ? $this->defaultVariant->id : '', |
| 1051 |
'data-variation-type' => $this->product->detail->variation_type, |
| 1052 |
'data-payment-type' => 'onetime', |
| 1053 |
'class' => 'fct-product-quantity-container' |
| 1054 |
]; |
| 1055 |
|
| 1056 |
$defaultVariantData = $this->getDefaultVariantData(); |
| 1057 |
|
| 1058 |
if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') { |
| 1059 |
$attributes['class'] .= ' is-hidden'; |
| 1060 |
} |
| 1061 |
|
| 1062 |
do_action('fluent_cart/product/single/before_quantity_block', [ |
| 1063 |
'product' => $this->product, |
| 1064 |
'scope' => 'product_quantity_block' |
| 1065 |
]); |
| 1066 |
?> |
| 1067 |
<div <?php $this->renderAttributes($attributes); ?>> |
| 1068 |
<label for="fct-product-qty-input" class="quantity-title"> |
| 1069 |
<?php esc_html_e('Quantity', 'fluent-cart'); ?> |
| 1070 |
</label> |
| 1071 |
|
| 1072 |
<div class="fct-product-quantity"> |
| 1073 |
<button class="fct-quantity-decrease-button" |
| 1074 |
data-fluent-cart-product-qty-decrease-button |
| 1075 |
title="<?php esc_html_e('Decrease Quantity', 'fluent-cart'); ?>" |
| 1076 |
aria-label="<?php esc_attr_e('Decrease Quantity', 'fluent-cart'); ?>" |
| 1077 |
> |
| 1078 |
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="0 0 14 2" fill="none"> |
| 1079 |
<path d="M12.3333 1L1.66659 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" |
| 1080 |
stroke-linejoin="round"></path> |
| 1081 |
</svg> |
| 1082 |
</button> |
| 1083 |
|
| 1084 |
<input |
| 1085 |
id="fct-product-qty-input" |
| 1086 |
min="1" |
| 1087 |
<?php echo $soldIndividually ? 'max="1"' : ''; ?> |
| 1088 |
class="fct-quantity-input" |
| 1089 |
data-fluent-cart-single-product-page-product-quantity-input |
| 1090 |
type="number" |
| 1091 |
inputmode="numeric" |
| 1092 |
pattern="[0-9]*" |
| 1093 |
placeholder="<?php esc_attr_e('Quantity', 'fluent-cart'); ?>" |
| 1094 |
value="1" |
| 1095 |
aria-label="<?php esc_attr_e('Product quantity', 'fluent-cart'); ?>" |
| 1096 |
/> |
| 1097 |
|
| 1098 |
<button class="fct-quantity-increase-button" |
| 1099 |
data-fluent-cart-product-qty-increase-button |
| 1100 |
title="<?php esc_attr_e('Increase Quantity', 'fluent-cart'); ?>" |
| 1101 |
aria-label="<?php esc_attr_e('Increase Quantity', 'fluent-cart'); ?>" |
| 1102 |
> |
| 1103 |
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"> |
| 1104 |
<path d="M6.99996 1.66666L6.99996 12.3333M12.3333 6.99999L1.66663 6.99999" stroke="currentColor" |
| 1105 |
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> |
| 1106 |
</svg> |
| 1107 |
</button> |
| 1108 |
</div> |
| 1109 |
</div> |
| 1110 |
<?php |
| 1111 |
do_action('fluent_cart/product/single/after_quantity_block', [ |
| 1112 |
'product' => $this->product, |
| 1113 |
'scope' => 'product_quantity_block' |
| 1114 |
]); |
| 1115 |
} |
| 1116 |
|
| 1117 |
public function renderPurchaseButtons($atts = []) |
| 1118 |
{ |
| 1119 |
$buyNowButtonAtts = $atts; |
| 1120 |
$this->renderBuyNowButton($buyNowButtonAtts); |
| 1121 |
$this->renderAddToCartButton($atts); |
| 1122 |
} |
| 1123 |
|
| 1124 |
public function renderBuyNowButton($atts = []) |
| 1125 |
{ |
| 1126 |
// Stock management check using isStock() method |
| 1127 |
// if (ModuleSettings::isActive('stock_management')) { |
| 1128 |
// if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) { |
| 1129 |
// if (!$this->defaultVariant->isStock()) { |
| 1130 |
// echo '<span aria-disabled="true">' . esc_html__('Out of stock', 'fluent-cart') . '</span>'; |
| 1131 |
// return; |
| 1132 |
// } |
| 1133 |
// } |
| 1134 |
// } |
| 1135 |
|
| 1136 |
$defaults = [ |
| 1137 |
'buy_now_text' => __('Buy Now', 'fluent-cart'), |
| 1138 |
'add_to_cart_text' => __('Add To Cart', 'fluent-cart'), |
| 1139 |
]; |
| 1140 |
|
| 1141 |
$atts = wp_parse_args($atts, $defaults); |
| 1142 |
|
| 1143 |
$enableModalCheckout = Helper::isModalCheckoutEnabled(); |
| 1144 |
|
| 1145 |
$isInStock = true; |
| 1146 |
if (ModuleSettings::isActive('stock_management')) { |
| 1147 |
$isInStock = $this->product->isStock() && ($this->defaultVariant && $this->defaultVariant->isStock()); |
| 1148 |
} |
| 1149 |
|
| 1150 |
$stockStatus = $isInStock ? 'in-stock' : 'out-of-stock'; |
| 1151 |
|
| 1152 |
$variationClass = 'fluent-cart-direct-checkout-button'; |
| 1153 |
if (!$isInStock) { |
| 1154 |
$variationClass .= ' is-hidden'; |
| 1155 |
} |
| 1156 |
|
| 1157 |
$buyNowAttributes = [ |
| 1158 |
'data-fluent-cart-direct-checkout-button' => '', |
| 1159 |
'data-variation-type' => $this->product->detail->variation_type, |
| 1160 |
'class' => $variationClass, |
| 1161 |
'data-stock-availability' => $stockStatus, |
| 1162 |
'data-quantity' => '1', |
| 1163 |
'data-cart-id' => $this->defaultVariant ? $this->defaultVariant->id : '', |
| 1164 |
'data-url' => site_url('?fluent-cart=instant_checkout&item_id='), |
| 1165 |
]; |
| 1166 |
|
| 1167 |
if ($isInStock) { |
| 1168 |
$buyNowAttributes['href'] = site_url('?fluent-cart=instant_checkout&item_id=') . ($this->defaultVariant ? $this->defaultVariant->id : '') . '&quantity=1'; |
| 1169 |
} |
| 1170 |
|
| 1171 |
if ($enableModalCheckout) { |
| 1172 |
$buyNowAttributes['data-fct-instant-checkout-button'] = ''; |
| 1173 |
$buyNowAttributes['data-enable-modal-checkout'] = 'yes'; |
| 1174 |
} |
| 1175 |
|
| 1176 |
$buyButtonText = apply_filters('fluent_cart/product/buy_now_button_text', $atts['buy_now_text'], [ |
| 1177 |
'product' => $this->product |
| 1178 |
]); |
| 1179 |
|
| 1180 |
?> |
| 1181 |
<?php |
| 1182 |
$variantTitle = $this->defaultVariant ? $this->defaultVariant->variation_title : ''; |
| 1183 |
$buyNowAriaLabel = $variantTitle |
| 1184 |
? sprintf( |
| 1185 |
/* translators: 1: Button text (e.g. "Buy Now"), 2: Variant name */ |
| 1186 |
__('%1$s - %2$s', 'fluent-cart'), |
| 1187 |
$buyButtonText, |
| 1188 |
$variantTitle |
| 1189 |
) |
| 1190 |
: $buyButtonText; |
| 1191 |
?> |
| 1192 |
<a <?php $this->renderAttributes($buyNowAttributes); ?> aria-label="<?php echo esc_attr($buyNowAriaLabel); ?>"> |
| 1193 |
<?php echo wp_kses_post($buyButtonText); ?> |
| 1194 |
</a> |
| 1195 |
<?php |
| 1196 |
} |
| 1197 |
|
| 1198 |
public function renderBuyNowButtonBlock($atts = []) |
| 1199 |
{ |
| 1200 |
$text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart')); |
| 1201 |
$variantIds = Arr::get($atts, 'variant_ids', []); |
| 1202 |
$variantId = Arr::get($variantIds, 0); |
| 1203 |
|
| 1204 |
$defaults = [ |
| 1205 |
'buy_now_text' => $text, |
| 1206 |
'class' => '', |
| 1207 |
'target' => '', |
| 1208 |
'rel' => '', |
| 1209 |
'is_shortcode' => false, |
| 1210 |
]; |
| 1211 |
|
| 1212 |
$atts = wp_parse_args($atts, $defaults); |
| 1213 |
|
| 1214 |
$enableModalCheckout = Arr::get($atts, 'enable_modal_checkout', false); |
| 1215 |
|
| 1216 |
$isInStock = true; |
| 1217 |
if (ModuleSettings::isActive('stock_management')) { |
| 1218 |
$isInStock = $this->product->isStock() && ($this->defaultVariant && $this->defaultVariant->isStock()); |
| 1219 |
} |
| 1220 |
$stockStatus = $isInStock ? 'in-stock' : 'out-of-stock'; |
| 1221 |
|
| 1222 |
$checkoutUrl = add_query_arg([ |
| 1223 |
'fluent-cart' => $enableModalCheckout ? 'modal_checkout' : 'instant_checkout', |
| 1224 |
'item_id' => $variantId ?? '', |
| 1225 |
'quantity' => 1 |
| 1226 |
], site_url()); |
| 1227 |
|
| 1228 |
$buyNowClass = trim('wp-block-button__link wp-element-button ' . Arr::get($atts, 'class', '')); |
| 1229 |
if ($stockStatus === 'out-of-stock') { |
| 1230 |
$buyNowClass .= ' out-of-stock'; |
| 1231 |
} |
| 1232 |
|
| 1233 |
$buyNowAttributes = [ |
| 1234 |
'data-fluent-cart-direct-checkout-button' => '', |
| 1235 |
'data-variation-type' => $this->product->detail->variation_type, |
| 1236 |
'class' => $buyNowClass, |
| 1237 |
'data-stock-availability' => $stockStatus, |
| 1238 |
'data-quantity' => '1', |
| 1239 |
'data-cart-id' => $variantId ?? '', |
| 1240 |
'data-url' => $checkoutUrl, |
| 1241 |
]; |
| 1242 |
|
| 1243 |
if ($stockStatus === 'out-of-stock') { |
| 1244 |
$buyNowAttributes['aria-disabled'] = 'true'; |
| 1245 |
} else { |
| 1246 |
$buyNowAttributes['href'] = $checkoutUrl; |
| 1247 |
} |
| 1248 |
|
| 1249 |
$target = Arr::get($atts, 'target'); |
| 1250 |
if ($target) { |
| 1251 |
$buyNowAttributes['target'] = $target; |
| 1252 |
if (strtolower($target) === '_blank') { |
| 1253 |
$buyNowAttributes['rel'] = Arr::get($atts, 'rel', 'noopener noreferrer'); |
| 1254 |
} |
| 1255 |
} |
| 1256 |
if ($enableModalCheckout) { |
| 1257 |
$buyNowAttributes['data-fct-instant-checkout-button'] = ''; |
| 1258 |
$buyNowAttributes['data-enable-modal-checkout'] = 'yes'; |
| 1259 |
} |
| 1260 |
$wrapperAttributes = ''; |
| 1261 |
$isShortcode = !empty($atts['is_shortcode']); |
| 1262 |
|
| 1263 |
if ($isShortcode) { |
| 1264 |
foreach ($buyNowAttributes as $attr => $value) { |
| 1265 |
if ($value === '') { |
| 1266 |
$wrapperAttributes .= esc_attr($attr) . ' '; |
| 1267 |
} else { |
| 1268 |
$wrapperAttributes .= sprintf('%s="%s" ', esc_attr($attr), esc_attr((string)$value)); |
| 1269 |
} |
| 1270 |
} |
| 1271 |
$wrapperAttributes = trim($wrapperAttributes); |
| 1272 |
} else { |
| 1273 |
$wrapperAttributes = get_block_wrapper_attributes($buyNowAttributes); |
| 1274 |
} |
| 1275 |
|
| 1276 |
$buyButtonText = apply_filters('fluent_cart/product/buy_now_button_text', $atts['buy_now_text'], [ |
| 1277 |
'product' => $this->product |
| 1278 |
]); |
| 1279 |
?> |
| 1280 |
<a <?php echo($wrapperAttributes); ?> aria-label="<?php echo esc_attr($buyButtonText); ?>"> |
| 1281 |
<?php echo wp_kses_post($buyButtonText); ?> |
| 1282 |
</a> |
| 1283 |
<?php |
| 1284 |
} |
| 1285 |
|
| 1286 |
public function renderAddToCartButton($atts = []) |
| 1287 |
{ |
| 1288 |
$defaults = [ |
| 1289 |
'buy_now_text' => __('Buy Now', 'fluent-cart'), |
| 1290 |
'add_to_cart_text' => __('Add To Cart', 'fluent-cart'), |
| 1291 |
]; |
| 1292 |
|
| 1293 |
$atts = wp_parse_args($atts, $defaults); |
| 1294 |
|
| 1295 |
$cartAttributes = [ |
| 1296 |
'data-fluent-cart-add-to-cart-button' => '', |
| 1297 |
'data-cart-id' => $this->defaultVariant ? $this->defaultVariant->id : '', |
| 1298 |
'data-product-id' => $this->product->ID, |
| 1299 |
'class' => 'fluent-cart-add-to-cart-button', |
| 1300 |
'data-variation-type' => $this->product->detail->variation_type, |
| 1301 |
]; |
| 1302 |
|
| 1303 |
$defaultVariantData = $this->getDefaultVariantData(); |
| 1304 |
|
| 1305 |
// If product is subscription-only, hide add-to-cart |
| 1306 |
if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') { |
| 1307 |
$cartAttributes['class'] .= ' is-hidden'; |
| 1308 |
} |
| 1309 |
|
| 1310 |
// Check stock availability using both product-level and variant-level |
| 1311 |
$isOutOfStock = false; |
| 1312 |
if (ModuleSettings::isActive('stock_management')) { |
| 1313 |
if (!$this->product->isStock() || ($this->defaultVariant && !$this->defaultVariant->isStock())) { |
| 1314 |
$isOutOfStock = true; |
| 1315 |
$cartAttributes['disabled'] = 'disabled'; |
| 1316 |
$cartAttributes['class'] .= ' out-of-stock'; |
| 1317 |
$cartAttributes['aria-disabled'] = 'true'; |
| 1318 |
$atts['add_to_cart_text'] = __('Not Available', 'fluent-cart'); |
| 1319 |
} |
| 1320 |
} |
| 1321 |
|
| 1322 |
$addToCartText = apply_filters('fluent_cart/product/add_to_cart_text', $atts['add_to_cart_text'], [ |
| 1323 |
'product' => $this->product |
| 1324 |
]); |
| 1325 |
// Render add to cart if product supports onetime, or if out of stock (to show "Not Available") |
| 1326 |
if ($this->hasOnetime || $isOutOfStock) : |
| 1327 |
?> |
| 1328 |
<?php |
| 1329 |
$variantTitle = $this->defaultVariant ? $this->defaultVariant->variation_title : ''; |
| 1330 |
$addToCartAriaLabel = $variantTitle |
| 1331 |
? sprintf( |
| 1332 |
/* translators: 1: Button text (e.g. "Add To Cart"), 2: Variant name */ |
| 1333 |
__('%1$s - %2$s', 'fluent-cart'), |
| 1334 |
$addToCartText, |
| 1335 |
$variantTitle |
| 1336 |
) |
| 1337 |
: $addToCartText; |
| 1338 |
?> |
| 1339 |
<button <?php $this->renderAttributes($cartAttributes); ?> |
| 1340 |
aria-label="<?php echo esc_attr($addToCartAriaLabel); ?>"> |
| 1341 |
<span class="text"> |
| 1342 |
<?php echo wp_kses_post($addToCartText); ?> |
| 1343 |
</span> |
| 1344 |
<span class="fluent-cart-loader" role="status"> |
| 1345 |
<svg aria-hidden="true" |
| 1346 |
width="20" |
| 1347 |
height="20" |
| 1348 |
class="w-5 h-5 text-gray-200 animate-spin fill-blue-600" |
| 1349 |
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 1350 |
<path |
| 1351 |
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" |
| 1352 |
fill="currentColor"/> |
| 1353 |
<path |
| 1354 |
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" |
| 1355 |
fill="currentFill"/> |
| 1356 |
</svg> |
| 1357 |
</span> |
| 1358 |
</button> |
| 1359 |
<?php |
| 1360 |
endif; |
| 1361 |
} |
| 1362 |
|
| 1363 |
public function renderAddToCartButtonBlock($atts = []) |
| 1364 |
{ |
| 1365 |
|
| 1366 |
$text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart')); |
| 1367 |
$extraClass = trim(Arr::get($atts, 'class', '')); |
| 1368 |
|
| 1369 |
$defaults = [ |
| 1370 |
'add_to_cart_text' => $text, |
| 1371 |
]; |
| 1372 |
|
| 1373 |
$atts = wp_parse_args($atts, $defaults); |
| 1374 |
|
| 1375 |
$cartAttributes = [ |
| 1376 |
'data-fluent-cart-add-to-cart-button' => '', |
| 1377 |
'class' => 'wp-block-button__link wp-element-button fct-loader', |
| 1378 |
'data-cart-id' => $this->defaultVariant ? $this->defaultVariant->id : '', |
| 1379 |
'data-product-id' => $this->product->ID, |
| 1380 |
'data-variation-type' => $this->product->detail->variation_type, |
| 1381 |
]; |
| 1382 |
|
| 1383 |
if ($extraClass) { |
| 1384 |
$cartAttributes['class'] .= ' ' . $extraClass; |
| 1385 |
} |
| 1386 |
|
| 1387 |
// If the product does NOT support one-time purchase |
| 1388 |
if (!$this->hasOnetime) { |
| 1389 |
if (Helper::isAdminUser()) { |
| 1390 |
$view = '<p class="fct-admin-notice">' . esc_html__('Add to Cart is not supported for subscription product', 'fluent-cart') . '</p>'; |
| 1391 |
|
| 1392 |
FrontendView::make('', $view); |
| 1393 |
return; |
| 1394 |
} |
| 1395 |
|
| 1396 |
return; |
| 1397 |
} |
| 1398 |
|
| 1399 |
// Check stock availability using both product-level and variant-level |
| 1400 |
if (ModuleSettings::isActive('stock_management')) { |
| 1401 |
if (!$this->product->isStock() || ($this->defaultVariant && !$this->defaultVariant->isStock())) { |
| 1402 |
$cartAttributes['disabled'] = 'disabled'; |
| 1403 |
$cartAttributes['class'] .= ' out-of-stock'; |
| 1404 |
$cartAttributes['aria-disabled'] = 'true'; |
| 1405 |
$atts['add_to_cart_text'] = __('Not Available', 'fluent-cart'); |
| 1406 |
} |
| 1407 |
} |
| 1408 |
|
| 1409 |
$addToCartText = apply_filters('fluent_cart/product/add_to_cart_text', $atts['add_to_cart_text'], [ |
| 1410 |
'product' => $this->product |
| 1411 |
]); |
| 1412 |
|
| 1413 |
$wrapperAttributes = ''; |
| 1414 |
$isShortcode = !empty($atts['is_shortcode']); |
| 1415 |
|
| 1416 |
if ($isShortcode) { |
| 1417 |
foreach ($cartAttributes as $attr => $value) { |
| 1418 |
if ($value === '') { |
| 1419 |
$wrapperAttributes .= esc_attr($attr) . ' '; |
| 1420 |
} else { |
| 1421 |
$wrapperAttributes .= sprintf('%s="%s" ', esc_attr($attr), esc_attr((string)$value)); |
| 1422 |
} |
| 1423 |
} |
| 1424 |
$wrapperAttributes = trim($wrapperAttributes); |
| 1425 |
} else { |
| 1426 |
$wrapperAttributes = get_block_wrapper_attributes($cartAttributes); |
| 1427 |
} |
| 1428 |
|
| 1429 |
?> |
| 1430 |
|
| 1431 |
<button <?php echo $wrapperAttributes; ?> |
| 1432 |
aria-label="<?php echo esc_attr($addToCartText); ?>"> |
| 1433 |
<span class="text"> |
| 1434 |
<?php echo wp_kses_post($addToCartText); ?> |
| 1435 |
</span> |
| 1436 |
<span class="fluent-cart-loader" role="status"> |
| 1437 |
<svg aria-hidden="true" |
| 1438 |
width="20" |
| 1439 |
height="20" |
| 1440 |
class="w-5 h-5 text-gray-200 animate-spin fill-blue-600" |
| 1441 |
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 1442 |
<path |
| 1443 |
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" |
| 1444 |
fill="currentColor"/> |
| 1445 |
<path |
| 1446 |
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" |
| 1447 |
fill="currentFill"/> |
| 1448 |
</svg> |
| 1449 |
</span> |
| 1450 |
</button> |
| 1451 |
|
| 1452 |
<?php |
| 1453 |
} |
| 1454 |
|
| 1455 |
public static function renderNoProductFound() |
| 1456 |
{ |
| 1457 |
?> |
| 1458 |
<div class="fluent-cart-shop-no-result-found" data-fluent-cart-shop-no-result-found role="status" |
| 1459 |
aria-live="polite"> |
| 1460 |
<p class="has-text-align-center has-large-font-size m-0"> |
| 1461 |
<?php echo esc_html__('No Product Found!', 'fluent-cart'); ?> |
| 1462 |
</p> |
| 1463 |
|
| 1464 |
<p class="has-text-align-center"> |
| 1465 |
<?php echo esc_html__('You can try clearing any filters.', 'fluent-cart'); ?> |
| 1466 |
</p> |
| 1467 |
</div> |
| 1468 |
<?php |
| 1469 |
} |
| 1470 |
|
| 1471 |
protected function renderVariationItem(ProductVariation $variant, $defaultId = '', $extraClasses = []) |
| 1472 |
{ |
| 1473 |
$availableStocks = $variant->available; |
| 1474 |
if (!$variant->manage_stock) { |
| 1475 |
$availableStocks = 'unlimited'; |
| 1476 |
} |
| 1477 |
|
| 1478 |
$comparePrice = $variant->compare_price; |
| 1479 |
if ($comparePrice <= $variant->item_price) { |
| 1480 |
$comparePrice = ''; |
| 1481 |
} |
| 1482 |
|
| 1483 |
if ($comparePrice) { |
| 1484 |
$comparePrice = Helper::toDecimal($comparePrice); |
| 1485 |
} |
| 1486 |
|
| 1487 |
$paymentType = Arr::get($variant->other_info, 'payment_type'); |
| 1488 |
|
| 1489 |
$itemClasses = [ |
| 1490 |
'fct-product-variant-item', |
| 1491 |
'fct_price_type_' . $paymentType, |
| 1492 |
'fct_variation_view_type_' . $this->viewType, |
| 1493 |
]; |
| 1494 |
|
| 1495 |
if ($variant->media_id) { |
| 1496 |
$itemClasses[] = 'fct-item-has-image'; |
| 1497 |
} |
| 1498 |
|
| 1499 |
if ($variant->id == $defaultId) { |
| 1500 |
$itemClasses[] = 'selected'; |
| 1501 |
} |
| 1502 |
|
| 1503 |
$priceSuffix = apply_filters('fluent_cart/product/price_suffix_atts', '', [ |
| 1504 |
'product' => $this->product, |
| 1505 |
'variant' => $variant, |
| 1506 |
'scope' => 'variant_item' |
| 1507 |
]); |
| 1508 |
|
| 1509 |
$renderingAttributes = [ |
| 1510 |
'data-fluent-cart-product-variant' => '', |
| 1511 |
'data-cart-id' => $variant->id, |
| 1512 |
'data-item-stock' => $variant->isStock() ? 'in-stock' : 'out-of-stock', |
| 1513 |
'data-default-variation-id' => $defaultId, |
| 1514 |
'data-payment-type' => $paymentType, |
| 1515 |
'data-available-stock' => $availableStocks, |
| 1516 |
'data-item-price' => Helper::toDecimal($variant->item_price), |
| 1517 |
'data-compare-price' => $comparePrice, |
| 1518 |
'data-price-suffix' => $priceSuffix, |
| 1519 |
'data-stock-management' => ModuleSettings::isActive('stock_management') ? 'yes' : 'no', |
| 1520 |
'data-sku' => $variant->sku ?? '', |
| 1521 |
'data-package-info' => $this->getVariantPackageInfoJson($variant), |
| 1522 |
]; |
| 1523 |
|
| 1524 |
if ($paymentType === 'subscription') { |
| 1525 |
$renderingAttributes['data-subscription-terms'] = $variant->getSubscriptionTermsText(true); |
| 1526 |
$repeatInterval = Arr::get($variant->other_info, 'repeat_interval', ''); |
| 1527 |
$hasInstallment = Arr::get($variant->other_info, 'has_installment') === 'yes'; |
| 1528 |
|
| 1529 |
$itemClasses[] = 'fct_sub_interval_' . $repeatInterval; |
| 1530 |
if ($hasInstallment) { |
| 1531 |
$itemClasses[] = 'fct_sub_has_installment'; |
| 1532 |
} |
| 1533 |
} |
| 1534 |
|
| 1535 |
if ($extraClasses) { |
| 1536 |
$itemClasses = array_merge($itemClasses, $extraClasses); |
| 1537 |
} |
| 1538 |
|
| 1539 |
$itemClasses = array_filter($itemClasses); |
| 1540 |
$renderingAttributes['class'] = implode(' ', $itemClasses); |
| 1541 |
|
| 1542 |
$itemPrice = $variant->item_price; |
| 1543 |
$comparePrice = $variant->compare_price; |
| 1544 |
if (!$comparePrice || $comparePrice <= $itemPrice) { |
| 1545 |
$comparePrice = 0; |
| 1546 |
} |
| 1547 |
|
| 1548 |
?> |
| 1549 |
<div |
| 1550 |
<?php $this->renderAttributes($renderingAttributes); ?> |
| 1551 |
role="radio" |
| 1552 |
tabindex="<?php echo $variant->id == $defaultId ? '0' : '-1'; ?>" |
| 1553 |
aria-checked="<?php echo $variant->id == $defaultId ? 'true' : 'false'; ?>" |
| 1554 |
aria-label="<?php echo esc_attr($variant->variation_title); ?>" |
| 1555 |
> |
| 1556 |
<?php if ($this->viewType === 'image'): ?> |
| 1557 |
<?php $this->renderTooltip($variant); ?> |
| 1558 |
<?php endif; ?> |
| 1559 |
|
| 1560 |
<div class="variant-content"> |
| 1561 |
<?php |
| 1562 |
if ($this->viewType === 'both' || $this->viewType === 'image') { |
| 1563 |
$this->renderVariantImage($variant); |
| 1564 |
} |
| 1565 |
?> |
| 1566 |
<?php |
| 1567 |
if ($this->viewType === 'both' || $this->viewType === 'text') { |
| 1568 |
echo '<div class="fct-product-variant-title" aria-label="' . esc_attr(__('Variant title', 'fluent-cart')) . '">' . esc_html($variant->variation_title) . '</div>'; |
| 1569 |
} |
| 1570 |
?> |
| 1571 |
</div> |
| 1572 |
|
| 1573 |
<?php if ($this->viewType === 'text' && $paymentType === 'subscription' && $this->columnType === 'one'): ?> |
| 1574 |
|
| 1575 |
<?php $this->renderSubscriptionInfo($variant); ?> |
| 1576 |
<?php endif; ?> |
| 1577 |
|
| 1578 |
<?php if ($this->viewType === 'text' && $this->columnType === 'one'): ?> |
| 1579 |
<div class="fct-product-variant-price"> |
| 1580 |
<?php if ($comparePrice): ?> |
| 1581 |
<div class="fct-product-variant-compare-price"> |
| 1582 |
<del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>"> |
| 1583 |
<span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del> |
| 1584 |
</div> |
| 1585 |
<?php endif; ?> |
| 1586 |
<div class="fct-product-variant-item-price" |
| 1587 |
aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>"> |
| 1588 |
<span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span> |
| 1589 |
</div> |
| 1590 |
</div> |
| 1591 |
<?php endif; ?> |
| 1592 |
</div> |
| 1593 |
<?php |
| 1594 |
} |
| 1595 |
|
| 1596 |
protected function renderTooltip($variant) |
| 1597 |
{ |
| 1598 |
?> |
| 1599 |
<div class="fct-product-variant-tooltip" role="tooltip" id="tooltip-<?php echo esc_attr($variant->id); ?>"> |
| 1600 |
<?php echo esc_html($variant->variation_title); ?> |
| 1601 |
</div> |
| 1602 |
<?php |
| 1603 |
} |
| 1604 |
|
| 1605 |
public function renderVariantImage($variant) |
| 1606 |
{ |
| 1607 |
$image = $variant->thumbnail; |
| 1608 |
if (!$image) { |
| 1609 |
$image = Vite::getAssetUrl('images/placeholder.svg'); |
| 1610 |
} |
| 1611 |
?> |
| 1612 |
<div class="fct-product-variant-image"> |
| 1613 |
<img role="img" alt="<?php echo esc_attr($variant->variation_title); ?>" |
| 1614 |
src="<?php echo esc_url($image); ?>"/> |
| 1615 |
</div> |
| 1616 |
<?php |
| 1617 |
} |
| 1618 |
|
| 1619 |
protected function renderSubscriptionInfo($variant = null) |
| 1620 |
{ |
| 1621 |
|
| 1622 |
if(!$variant){ |
| 1623 |
return ''; |
| 1624 |
} |
| 1625 |
$info = $variant->getSubscriptionTermsText(true); |
| 1626 |
|
| 1627 |
if (!$info) { |
| 1628 |
return ''; |
| 1629 |
} |
| 1630 |
|
| 1631 |
?> |
| 1632 |
<div class="fct-product-variant-payment-type" aria-live="polite"> |
| 1633 |
<div class="additional-info"> |
| 1634 |
<span><?php echo esc_html($info); ?></span> |
| 1635 |
</div> |
| 1636 |
</div> |
| 1637 |
<?php |
| 1638 |
} |
| 1639 |
|
| 1640 |
protected function renderAttributes($atts = []) |
| 1641 |
{ |
| 1642 |
foreach ($atts as $attr => $value) { |
| 1643 |
if ($value !== '') { |
| 1644 |
echo esc_attr($attr) . '="' . esc_attr((string)$value) . '" '; |
| 1645 |
} else { |
| 1646 |
echo esc_attr($attr) . ' '; |
| 1647 |
} |
| 1648 |
} |
| 1649 |
} |
| 1650 |
|
| 1651 |
protected function renderTab($atts = []) |
| 1652 |
{ |
| 1653 |
?> |
| 1654 |
<div class="fct-product-tab" data-fluent-cart-product-tab> |
| 1655 |
<?php $this->renderTabNav(); ?> |
| 1656 |
|
| 1657 |
<div class="fct-product-tab-content" data-tab-contents> |
| 1658 |
<?php $this->renderTabPane($atts); ?> |
| 1659 |
</div> |
| 1660 |
</div> |
| 1661 |
<?php |
| 1662 |
|
| 1663 |
} |
| 1664 |
|
| 1665 |
protected function renderTabNav() |
| 1666 |
{ |
| 1667 |
?> |
| 1668 |
|
| 1669 |
<div class="fct-product-tab-nav" role="tablist"> |
| 1670 |
<div class="tab-active-bar" data-tab-active-bar></div> |
| 1671 |
<?php |
| 1672 |
foreach ($this->paymentTypes as $typeKey => $typeLabel) : ?> |
| 1673 |
<div |
| 1674 |
class="fct-product-tab-nav-item <?php echo esc_attr($this->activeTab === $typeKey ? 'active' : ''); ?>" |
| 1675 |
data-tab="<?php echo esc_attr($typeKey); ?>" |
| 1676 |
role="tab" |
| 1677 |
tabindex="<?php echo $this->activeTab === $typeKey ? '0' : '-1'; ?>" |
| 1678 |
aria-selected="<?php echo $this->activeTab === $typeKey ? 'true' : 'false'; ?>" |
| 1679 |
aria-controls="<?php echo esc_attr($typeKey); ?>" |
| 1680 |
> |
| 1681 |
<?php echo esc_html($typeLabel); ?> |
| 1682 |
</div> |
| 1683 |
<?php endforeach; |
| 1684 |
?> |
| 1685 |
</div> |
| 1686 |
|
| 1687 |
<?php |
| 1688 |
} |
| 1689 |
|
| 1690 |
protected function renderTabPane($atts = []) |
| 1691 |
{ |
| 1692 |
$variantsClasses = [ |
| 1693 |
'fct-product-variants', |
| 1694 |
'column-type-' . $this->columnType, |
| 1695 |
Arr::get($atts, 'wrapper_class', ''), |
| 1696 |
]; |
| 1697 |
|
| 1698 |
foreach ($this->variantsByPaymentTypes as $variantKey => $variants): ?> |
| 1699 |
<div |
| 1700 |
data-tab-content |
| 1701 |
id="<?php echo esc_attr($variantKey); ?>" |
| 1702 |
class="fct-product-tab-pane <?php echo esc_attr($this->activeTab === $variantKey ? 'active' : ''); ?>" |
| 1703 |
role="tabpanel" |
| 1704 |
aria-labelledby="<?php echo esc_attr($variantKey); ?>" |
| 1705 |
> |
| 1706 |
<div class="<?php echo esc_attr(implode(' ', $variantsClasses)); ?>" role="radiogroup" |
| 1707 |
aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>"> |
| 1708 |
<?php |
| 1709 |
//Convert to collection safely before sorting |
| 1710 |
$variants = (new Collection($variants))->sortBy('serial_index')->values(); |
| 1711 |
|
| 1712 |
foreach ($variants as $variant) { |
| 1713 |
do_action('fluent_cart/product/single/before_variant_item', [ |
| 1714 |
'product' => $this->product, |
| 1715 |
'variant' => $variant, |
| 1716 |
'scope' => 'product_variant_item' |
| 1717 |
]); |
| 1718 |
|
| 1719 |
$this->renderVariationItem($variant, $this->defaultVariationId); |
| 1720 |
|
| 1721 |
do_action('fluent_cart/product/single/after_variant_item', [ |
| 1722 |
'product' => $this->product, |
| 1723 |
'variant' => $variant, |
| 1724 |
'scope' => 'product_variant_item' |
| 1725 |
]); |
| 1726 |
} |
| 1727 |
?> |
| 1728 |
</div> |
| 1729 |
|
| 1730 |
</div> |
| 1731 |
<?php endforeach; ?> |
| 1732 |
|
| 1733 |
<?php |
| 1734 |
} |
| 1735 |
|
| 1736 |
protected function getDefaultVariantData() |
| 1737 |
{ |
| 1738 |
if (empty($this->variants) || !$this->defaultVariationId) { |
| 1739 |
return null; |
| 1740 |
} |
| 1741 |
|
| 1742 |
foreach ($this->variants as $variant) { |
| 1743 |
if ($variant['id'] == $this->defaultVariationId) { |
| 1744 |
return $variant; |
| 1745 |
} |
| 1746 |
} |
| 1747 |
|
| 1748 |
return null; |
| 1749 |
} |
| 1750 |
} |
| 1751 |
|