PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Services/Renderer/ProductCardRender.php +117 -21 1.6.3 → 1.7.1 View file →
@@ -4,10 +4,10 @@
4 4
5 5 use FluentCart\Api\ModuleSettings;
6 6 use FluentCart\App\Helpers\Helper;
7 7 use FluentCart\App\Models\Product;
8 +use FluentCart\App\Services\ProductReviewService;
8 9 use FluentCart\App\Modules\Templating\AssetLoader;
9 -use FluentCart\App\Vite;
10 10 use FluentCart\Framework\Support\Arr;
11 11
12 12 class ProductCardRender
13 13 {
@@ -86,8 +86,9 @@
86 86 __('%s product card', 'fluent-cart'), $this->product->post_title));
87 87 ?>">
88 88 <?php $this->renderProductImage(); ?>
89 89 <?php $this->renderTitle(); ?>
90 + <?php $this->renderStarRating(); ?>
90 91 <?php if (!Arr::get($this->config, 'hide_excerpt', false)) { $this->renderExcerpt(); } ?>
91 92 <?php $this->renderPrices(); ?>
92 93 <?php $this->showBuyButton(); ?>
93 94 </article>
@@ -134,8 +135,104 @@
134 135 $defaultVariant
135 136 );
136 137 }
137 138
139 + public function renderStarRating()
140 + {
141 + if (!ModuleSettings::isActive('reviews')) {
142 + return;
143 + }
144 +
145 + // Store-level toggles: Settings → Store Settings → Product Page →
146 + // Product Rating. Shop grid/carousels gate on show_rating_in_shop;
147 + // relevant/related product sections gate on show_rating_in_relevant
148 + // (callers tag those cards with config rating_context = 'relevant').
149 + // The explicit product-rating block (renderStarRatingBlock) stays
150 + // unaffected — it is user-placed.
151 + $ratingContext = Arr::get($this->config, 'rating_context', 'shop');
152 +
153 + $ratingVisibilitySettingKey = $ratingContext === 'relevant'
154 + ? 'show_rating_in_relevant'
155 + : 'show_rating_in_shop';
156 +
157 + if ((new \FluentCart\Api\StoreSettings())->get($ratingVisibilitySettingKey, 'yes') !== 'yes') {
158 + return;
159 + }
160 +
161 + if (!ProductReviewService::ratingMeetsThresholds($this->product, $this->config)) {
162 + return;
163 + }
164 +
165 + $otherInfo = $this->product->detail->other_info ?: [];
166 + $avgRating = Arr::get($otherInfo, 'average_rating', 0);
167 + $reviewCount = (int) Arr::get($otherInfo, 'review_count', 0);
168 +
169 + if ($reviewCount < 1) {
170 + return;
171 + }
172 +
173 + $fullStars = (int) floor($avgRating);
174 + $halfStar = ($avgRating - $fullStars) >= 0.5;
175 + $emptyStars = 5 - $fullStars - ($halfStar ? 1 : 0);
176 +
177 + ?>
178 + <div class="fct-product-card-rating" role="img" aria-label="<?php echo esc_attr(sprintf(
179 + /* translators: %1$s: average rating, %2$d: review count */
180 + __('Rated %1$s out of 5 based on %2$d reviews', 'fluent-cart'), $avgRating, $reviewCount)); ?>">
181 + <span class="fct-product-card-stars" aria-hidden="true">
182 + <?php
183 + for ($i = 0; $i < $fullStars; $i++) {
184 + echo '<span class="fct-star fct-star-filled">' . ReviewThreadMarkup::starSvg() . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
185 + }
186 + if ($halfStar) {
187 + echo '<span class="fct-star fct-star-half"><span class="fct-star-half-empty">' . ReviewThreadMarkup::starSvg() . '</span><span class="fct-star-half-fill">' . ReviewThreadMarkup::starSvg() . '</span></span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
188 + }
189 + for ($i = 0; $i < $emptyStars; $i++) {
190 + echo '<span class="fct-star fct-star-empty">' . ReviewThreadMarkup::starSvg() . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
191 + }
192 + ?>
193 + </span>
194 + <span class="fct-product-card-review-count">(<?php echo esc_html($reviewCount); ?>)</span>
195 + </div>
196 + <?php
197 + }
198 +
199 + public function renderStarRatingBlock($wrapperAttributes = '', array $attributes = [], $block = null)
200 + {
201 + if (!ProductReviewService::ratingMeetsThresholds($this->product, $attributes, $block)) {
202 + return;
203 + }
204 +
205 + $otherInfo = $this->product->detail->other_info ?: [];
206 + $avgRating = Arr::get($otherInfo, 'average_rating', 0);
207 + $reviewCount = (int) Arr::get($otherInfo, 'review_count', 0);
208 +
209 + $fullStars = (int) floor($avgRating);
210 + $halfStar = ($avgRating - $fullStars) >= 0.5;
211 + $emptyStars = 5 - $fullStars - ($halfStar ? 1 : 0);
212 +
213 + ?>
214 + <div <?php echo $wrapperAttributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> role="img" aria-label="<?php echo esc_attr(sprintf(
215 + /* translators: %1$s: average rating, %2$d: review count */
216 + __('Rated %1$s out of 5 based on %2$d reviews', 'fluent-cart'), $avgRating, $reviewCount)); ?>">
217 + <span class="fct-product-card-stars" aria-hidden="true">
218 + <?php
219 + for ($i = 0; $i < $fullStars; $i++) {
220 + echo '<span class="fct-star fct-star-filled">' . ReviewThreadMarkup::starSvg() . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
221 + }
222 + if ($halfStar) {
223 + echo '<span class="fct-star fct-star-half"><span class="fct-star-half-empty">' . ReviewThreadMarkup::starSvg() . '</span><span class="fct-star-half-fill">' . ReviewThreadMarkup::starSvg() . '</span></span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
224 + }
225 + for ($i = 0; $i < $emptyStars; $i++) {
226 + echo '<span class="fct-star fct-star-empty">' . ReviewThreadMarkup::starSvg() . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
227 + }
228 + ?>
229 + </span>
230 + <span class="fct-product-card-review-count">(<?php echo esc_html($reviewCount); ?>)</span>
231 + </div>
232 + <?php
233 + }
234 +
138 235 public function renderExcerpt($atts = '')
139 236 {
140 237 if (empty($this->product->post_excerpt)) {
141 238 return;
@@ -210,13 +307,16 @@
210 307 if (!RenderGate::shouldRender('image', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
211 308 return;
212 309 }
213 310
214 - $image = $this->product->thumbnail;
311 + // Product::$thumbnail already falls back to the placeholder, which
312 + // hides whether this is one, so ask for the featured image directly.
313 + $detail = $this->product->detail;
314 + $image = $detail ? Arr::get((array) $detail->featured_media, 'url') : '';
215 315 $isPlaceholder = false;
216 316
217 317 if (!$image) {
218 - $image = Vite::getAssetUrl('images/placeholder.svg');
318 + $image = Helper::getProductPlaceholderUrl();
219 319 $isPlaceholder = true;
220 320 }
221 321
222 322 $altText = $isPlaceholder
@@ -229,9 +329,9 @@
229 329 'product' => $this->product,
230 330 'scope' => 'product_card'
231 331 ]));
232 332 ?>
233 - <a class="fct-product-card-image-wrap"
333 + <a class="fct-product-card-image-wrap<?php echo $isPlaceholder ? ' is-placeholder' : ''; ?>"
234 334 href="<?php echo esc_url($this->viewUrl); ?>"
235 335 style="display: block;"
236 336 aria-label="<?php echo esc_attr(sprintf(
237 337 /* translators: %s: product title */
@@ -294,12 +394,11 @@
294 394 class="fct-product-card-prices"
295 395 role="region"
296 396 aria-label="<?php echo esc_attr__('Product pricing', 'fluent-cart'); ?>">
297 397 <?php if ($comparePrice): ?>
298 - <span class="fct-compare-price" aria-label="<?php echo esc_attr(sprintf(
299 - /* translators: %s: product price */
300 - __('Original price: %s', 'fluent-cart'), $formattedComparePrice)); ?>">
301 - <del aria-hidden="true"><?php echo esc_html($formattedComparePrice); ?></del>
398 + <span class="fct-compare-price">
399 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
400 + <del><?php echo esc_html($formattedComparePrice); ?></del>
302 401 </span>
303 402 <?php endif; ?>
304 403
305 404 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
@@ -304,30 +403,27 @@
304 403
305 404 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
306 405 <!-- Case 2: price range -->
307 406 <?php if ($priceFormat === 'range'): ?>
308 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
309 - /* translators: %1$s: min price, %2$s: max price */
310 - __('Price range from %1$s to %2$s', 'fluent-cart'), $formattedMinPrice, $formattedMaxPrice)); ?>">
311 - <span aria-hidden="true"><?php echo esc_html($formattedMinPrice); ?> - <?php echo esc_html($formattedMaxPrice); ?></span>
407 + <span class="fct-item-price">
408 + <span class="fct-sr-only"><?php echo esc_html__('Price range:', 'fluent-cart'); ?></span>
409 + <?php echo esc_html($formattedMinPrice); ?>
410 + <span aria-hidden="true">-</span>
411 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
412 + <?php echo esc_html($formattedMaxPrice); ?>
312 413 </span>
313 414 <?php else: ?>
314 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
315 - /* translators: %s: min price */
316 - __('Starting from %s', 'fluent-cart'), $formattedMinPrice)); ?>">
317 - <span aria-hidden="true"><?php
415 + <span class="fct-item-price"><?php
318 416 /* translators: %s is the minimum price */
319 417 printf(esc_html__('From %s', 'fluent-cart'), esc_html($formattedMinPrice));
320 418 ?></span>
321 - </span>
322 419 <?php endif; ?>
323 420
324 421 <?php else: ?>
325 422 <!-- Case 3: Simple or single price -->
326 - <span class="fct-item-price" aria-label="<?php echo esc_attr(sprintf(
327 - /* translators: %s: product price */
328 - __('Price: %s', 'fluent-cart'), $formattedMinPrice)); ?>">
329 - <span aria-hidden="true"><?php echo esc_html($formattedMinPrice); ?></span>
423 + <span class="fct-item-price">
424 + <span class="fct-sr-only"><?php echo esc_html__('Price:', 'fluent-cart'); ?></span>
425 + <?php echo esc_html($formattedMinPrice); ?>
330 426 </span>
331 427 <?php endif; ?>
332 428
333 429 <?php