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