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
fluent-cart / app / Services / Renderer / ReviewModalRenderer.php

ReviewModalRenderer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Services/Renderer/ReviewModalRenderer.php

218 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Renderer;
4
5 use FluentCart\App\Helpers\Status;
6 use FluentCart\App\Models\ProductReview;
7 use FluentCart\App\Services\ProductReviewService;
8
9 /**
10 * Server-rendered markup for the review thread modal.
11 *
12 * The storefront JS only paints the overlay shell and a loader, then swaps
13 * in this output — the same split ProductModalRenderer uses for the product
14 * modal. Rendering here means the review, its replies and any extension
15 * footer arrive in one request instead of the modal opening empty and
16 * fetching replies afterwards.
17 */
18 class ReviewModalRenderer
19 {
20 protected $review;
21
22 protected $productName;
23
24 public function __construct(ProductReview $review, $productName = '')
25 {
26 $this->review = $review;
27 $this->productName = $productName;
28 }
29
30 public function render()
31 {
32 $heading = $this->productName
33 /* translators: %s - product name */
34 ? sprintf(__("%s's Review", 'fluent-cart'), $this->productName)
35 : __('Review Thread', 'fluent-cart');
36
37 $approvedReplies = $this->review->replies()
38 ->where('status', Status::REVIEW_APPROVED)
39 ->orderBy('created_at', 'ASC')
40 ->orderBy('id', 'ASC')
41 ->limit(50)
42 ->get();
43 ?>
44 <div class="fct-review-modal" data-review-modal tabindex="-1">
45 <div class="fct-review-modal-header" data-review-modal-header>
46 <h3 class="fct-review-modal-heading" data-review-modal-heading><?php echo esc_html($heading); ?></h3>
47 <button type="button" class="fct-review-modal-close" data-modal-close
48 aria-label="<?php esc_attr_e('Close', 'fluent-cart'); ?>">
49 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true" focusable="false"><path d="M12.8337 1.16663L1.16699 12.8333M1.16699 1.16663L12.8337 12.8333" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
50 </button>
51 </div>
52 <div class="fct-review-modal-body" data-review-modal-body>
53 <div class="fct-review-modal-original" data-review-original>
54 <?php $this->renderAuthorRow($this->review); ?>
55 <?php
56 // The item the review names, if any. Resolved here rather
57 // than by the caller so every route into this modal shows
58 // it; a single review is a one-row batch.
59 ProductReviewService::attachItemLabels([$this->review]);
60 $itemLabel = trim((string) $this->review->getAttribute('item_label'));
61 ?>
62 <?php if ($itemLabel !== '') : ?>
63 <div class="fct-review-modal-item" data-review-item><?php echo esc_html($itemLabel); ?></div>
64 <?php endif; ?>
65 <?php if ($this->review->title) : ?>
66 <div class="fct-review-modal-title" data-review-title><?php echo esc_html($this->review->title); ?></div>
67 <?php endif; ?>
68 <div class="fct-review-modal-content" data-review-content><?php echo esc_html($this->review->content); ?></div>
69 <?php $this->renderMedia($this->review); ?>
70 </div>
71 <div class="fct-review-modal-replies" data-review-replies>
72 <?php if ($approvedReplies->isEmpty()) : ?>
73 <div class="fct-review-modal-empty" data-review-replies-empty><?php esc_html_e('No replies yet.', 'fluent-cart'); ?></div>
74 <?php else : ?>
75 <?php foreach ($approvedReplies as $reply) : ?>
76 <?php $this->renderReply($reply); ?>
77 <?php endforeach; ?>
78 <?php endif; ?>
79 </div>
80 <?php
81 /**
82 * Extension point for the modal footer — Pro renders the
83 * reviewer reply form here when threaded replies are on.
84 *
85 * A single named-array payload, the shape the great majority
86 * of fluent_cart hooks use, so keys can be added later
87 * without changing the listener signature.
88 *
89 * @param array $payload ['review' => ProductReview]
90 */
91 do_action('fluent_cart/review/modal_footer', ['review' => $this->review]);
92 ?>
93 </div>
94 </div>
95 <?php
96 }
97
98 /**
99 * One reply row.
100 *
101 * Public so the reply endpoint can return the same markup for a reply
102 * just posted — the storefront appends it verbatim instead of rebuilding
103 * the structure in JS, where it would drift from this renderer.
104 */
105 public function renderReply(ProductReview $reply)
106 {
107 ?>
108 <div class="fct-review-modal-reply" data-review-reply data-reply-id="<?php echo esc_attr($reply->id); ?>">
109 <?php $this->renderAuthorRow($reply); ?>
110 <div class="fct-review-modal-content" data-review-content><?php echo esc_html($reply->content); ?></div>
111 </div>
112 <?php
113 }
114
115 /**
116 * Rendered markup for a single reply, for callers that need it as a
117 * string (the reply endpoint's JSON response).
118 */
119 public static function replyHtml(ProductReview $reply): string
120 {
121 ob_start();
122 (new static($reply))->renderReply($reply);
123
124 return (string) ob_get_clean();
125 }
126
127 /**
128 * Author line for a thread entry — the review itself or one of its
129 * replies, both of which are ProductReview rows.
130 */
131 /**
132 * The avatar for one thread row: the row's photo when there is one, a
133 * person icon as the placeholder underneath. A photo that fails to load
134 * — Gravatar default=404 does exactly that for an address with no
135 * avatar — removes itself and the icon shows, so the markup needs no
136 * script of its own.
137 *
138 * @param ProductReview $entry
139 * @param string $authorName
140 * @return void
141 */
142 protected function renderAvatar(ProductReview $entry, $authorName)
143 {
144 echo ReviewThreadMarkup::avatarHtml((string) $entry->photo); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
145 }
146
147 protected function renderAuthorRow(ProductReview $entry)
148 {
149 $authorName = $entry->reviewer_name ?: __('Anonymous', 'fluent-cart');
150 ?>
151 <div class="fct-review-modal-author-row" data-review-author-row>
152 <?php $this->renderAvatar($entry, $authorName); ?>
153 <div class="fct-review-modal-meta-text">
154 <div class="fct-review-modal-name-line">
155 <span class="fct-review-modal-author" data-review-author><?php echo esc_html($authorName); ?></span>
156 <?php if ($entry->rating > 0) : ?>
157 <span class="fct-review-modal-stars" data-review-stars><?php $this->renderStars((int) $entry->rating); ?></span>
158 <?php endif; ?>
159 </div>
160 <div class="fct-review-modal-date" data-review-date><?php echo esc_html($this->relativeDate($entry->created_at)); ?></div>
161 </div>
162 </div>
163 <?php
164 }
165
166 protected function renderMedia(ProductReview $entry)
167 {
168 $mediaItems = $entry->media;
169
170 if (empty($mediaItems)) {
171 return;
172 }
173 ?>
174 <div class="fct-review-modal-media" data-review-media>
175 <?php foreach ($mediaItems as $mediaItem) : ?>
176 <?php if (is_array($mediaItem)) : ?>
177 <?php echo ReviewThreadMarkup::mediaItemHtml($mediaItem, 'modal'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from escaped parts; filter consumers own their markup ?>
178 <?php endif; ?>
179 <?php endforeach; ?>
180 </div>
181 <?php
182 }
183
184 /**
185 * Star row, matching FluentCartProductReviews#getStarsHtml in Reviews.js:
186 * the same fct-star-filled / fct-star-empty classes the stylesheet colours,
187 * preceded by the screen-reader label, since the stars themselves are
188 * aria-hidden.
189 */
190 protected function renderStars($rating)
191 {
192 $rating = (int) $rating;
193
194 printf(
195 '<span class="fct-sr-only">%s</span>',
196 esc_html(sprintf(
197 /* translators: %d - star rating out of five */
198 __('Rated %d out of 5', 'fluent-cart'),
199 $rating
200 ))
201 );
202
203 for ($position = 1; $position <= 5; $position++) {
204 $starClass = $position <= $rating ? 'fct-star fct-star-filled' : 'fct-star fct-star-empty';
205 echo '<span class="' . esc_attr($starClass) . '" data-review-star aria-hidden="true">' . ReviewThreadMarkup::starSvg() . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
206 }
207 }
208
209 /**
210 * Matches the storefront's relative-time labels so a server-rendered
211 * modal reads the same as the JS-rendered list behind it.
212 */
213 protected function relativeDate($createdAt)
214 {
215 return ReviewThreadMarkup::relativeDate($createdAt);
216 }
217 }
218