| @@ -78,10 +78,27 @@ | ||
| 78 | 78 | |
| 79 | 79 | public static function register() |
| 80 | 80 | { |
| 81 | 81 | add_action('wp_enqueue_scripts', [self::class, 'enqueueAssets']); |
| 82 | + add_action('template_redirect', [self::class, 'markOrderReviewPageNoIndex']); | |
| 82 | 83 | } |
| 83 | 84 | |
| 85 | + /** | |
| 86 | + * The store's chosen order-review page is reached from an email with a | |
| 87 | + * bearer credential in its URL and must not be indexed. The shortcode | |
| 88 | + * renders inside the page body, after wp_head() has already printed the | |
| 89 | + * robots tag, so the page has to be flagged before the head — here, once | |
| 90 | + * the main query knows which page it is. | |
| 91 | + * | |
| 92 | + * @return void | |
| 93 | + */ | |
| 94 | + public static function markOrderReviewPageNoIndex() | |
| 95 | + { | |
| 96 | + if (TemplateService::getCurrentFcPageType() === 'order_review') { | |
| 97 | + add_filter('wp_robots', 'wp_robots_no_robots'); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 84 | 101 | public static function enqueueAssets() |
| 85 | 102 | { |
| 86 | 103 | $pageType = TemplateService::getCurrentFcPageType(); |
| 87 | 104 | |
| @@ -101,13 +118,141 @@ | ||
| 101 | 118 | break; |
| 102 | 119 | case 'checkout': |
| 103 | 120 | self::loadCheckoutAssets(); |
| 104 | 121 | break; |
| 122 | + case 'order_review': | |
| 123 | + // The store's chosen review page: enqueued here, in the head, | |
| 124 | + // rather than mid-body by the shortcode, so the form and the | |
| 125 | + // page do not paint unstyled first. | |
| 126 | + self::loadReviewSubmissionFormAssets(); | |
| 127 | + Vite::enqueueStyle('fluent-cart-order-review', 'public/order-review/order-review.scss'); | |
| 128 | + break; | |
| 105 | 129 | default: |
| 106 | 130 | return; |
| 107 | 131 | } |
| 108 | 132 | } |
| 109 | 133 | |
| 134 | + // Shared between the full single-product bundle and the review-form-only | |
| 135 | + // one, so a form rendered either way is enqueued under one handle and | |
| 136 | + // localized with one array. | |
| 137 | + const REVIEW_FORM_SCRIPT_HANDLE = 'fluent-cart-review-form'; | |
| 138 | + const REVIEW_FORM_STYLE_HANDLE = 'fluent-cart-reviews'; | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * Just enough to run a review form: the form script, the review | |
| 142 | + * stylesheet, and the strings the script reads. | |
| 143 | + * | |
| 144 | + * A review page is not a product page. loadSingleProductAssets() also | |
| 145 | + * carries gallery zoom, the single-product behaviour, the review list, | |
| 146 | + * the product-card assets and the add-to-cart and direct-checkout | |
| 147 | + * styles — none of which a standalone form uses, and all of which every | |
| 148 | + * visitor to that page would download and execute. | |
| 149 | + * | |
| 150 | + * Idempotent, and idempotent against the full bundle too: both enqueue | |
| 151 | + * the same file under REVIEW_FORM_SCRIPT_HANDLE, so whichever runs | |
| 152 | + * second is dropped by WordPress rather than loading the script twice | |
| 153 | + * and giving every form two controllers. | |
| 154 | + */ | |
| 155 | + public static function loadReviewSubmissionFormAssets() | |
| 156 | + { | |
| 157 | + self::markFrontendAssetsRequired(); | |
| 158 | + | |
| 159 | + if (wp_script_is(self::REVIEW_FORM_SCRIPT_HANDLE, 'enqueued')) { | |
| 160 | + return; | |
| 161 | + } | |
| 162 | + | |
| 163 | + // Through the same helper the full bundle uses, so the version and | |
| 164 | + // the localization are stamped on identically. | |
| 165 | + Vite::enqueueAllScripts( | |
| 166 | + [ | |
| 167 | + [ | |
| 168 | + 'source' => 'public/single-product/ReviewForm.js', | |
| 169 | + 'dependencies' => [], | |
| 170 | + 'inFooter' => true, | |
| 171 | + 'handle' => self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 172 | + ], | |
| 173 | + ], | |
| 174 | + self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 175 | + [ | |
| 176 | + 'fluentcart_review_vars' => static::reviewJsVars(), | |
| 177 | + ] | |
| 178 | + ); | |
| 179 | + | |
| 180 | + Vite::enqueueStyle( | |
| 181 | + self::REVIEW_FORM_STYLE_HANDLE, | |
| 182 | + 'public/single-product/reviews.scss' | |
| 183 | + ); | |
| 184 | + | |
| 185 | + /** | |
| 186 | + * The review form's assets are on this page. An add-on that extends | |
| 187 | + * the form (Pro's photo upload) enqueues its own script and styles | |
| 188 | + * here, so a surface that loads the form without being a product | |
| 189 | + * page — the customer dashboard, the order-review page — gets them | |
| 190 | + * too. Fired once per request, when the assets are first enqueued. | |
| 191 | + * | |
| 192 | + * @param array $context empty for now; the single payload every | |
| 193 | + * action carries, so listeners keep one | |
| 194 | + * signature as context is added | |
| 195 | + */ | |
| 196 | + do_action('fluent_cart/review/submission_form_assets_loaded', []); | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * @return array | |
| 201 | + */ | |
| 202 | + public static function reviewJsVars(): array | |
| 203 | + { | |
| 204 | + $reviewVars = [ | |
| 205 | + // Threaded replies (customer replies + multiple store replies) | |
| 206 | + // are a Pro capability, off by default. The frontend uses this to | |
| 207 | + // decide whether to render the reviewer reply form in the modal. | |
| 208 | + 'trans' => [ | |
| 209 | + 'no_reviews' => __('No reviews yet. Be the first to write a review!', 'fluent-cart'), | |
| 210 | + 'verified_purchase' => __('Verified Purchase', 'fluent-cart'), | |
| 211 | + 'rating_required' => __('Please select a rating', 'fluent-cart'), | |
| 212 | + 'photo_preview' => __('Photo preview', 'fluent-cart'), | |
| 213 | + /* translators: %s: number of attachments still behind the + */ | |
| 214 | + 'view_more_attachments' => __('View %s more attachments', 'fluent-cart'), | |
| 215 | + 'content_required' => __('Please write a review', 'fluent-cart'), | |
| 216 | + 'name_required' => __('Please enter your name', 'fluent-cart'), | |
| 217 | + 'email_required' => __('Please enter your email', 'fluent-cart'), | |
| 218 | + 'submitting' => __('Submitting...', 'fluent-cart'), | |
| 219 | + 'submit_review' => __('Submit Review', 'fluent-cart'), | |
| 220 | + 'store_reply' => __('Store Reply', 'fluent-cart'), | |
| 221 | + 'reviewer' => __('Reviewer', 'fluent-cart'), | |
| 222 | + 'reply' => __('Reply', 'fluent-cart'), | |
| 223 | + 'view_reply' => __('View Reply', 'fluent-cart'), | |
| 224 | + 'review_thread' => __('Review Thread', 'fluent-cart'), | |
| 225 | + 'no_replies' => __('No replies yet.', 'fluent-cart'), | |
| 226 | + 'close' => __('Close', 'fluent-cart'), | |
| 227 | + 'all' => __('All', 'fluent-cart'), | |
| 228 | + 'prev' => __('Prev', 'fluent-cart'), | |
| 229 | + 'next' => __('Next', 'fluent-cart'), | |
| 230 | + 'prev_page' => __('Previous page', 'fluent-cart'), | |
| 231 | + 'next_page' => __('Next page', 'fluent-cart'), | |
| 232 | + /* translators: 1: the page this segment goes to, 2: how many there are. */ | |
| 233 | + 'slider_page_of' => __('Page %1$s of %2$s', 'fluent-cart'), | |
| 234 | + 'read_more' => __('Read more', 'fluent-cart'), | |
| 235 | + 'show_less' => __('Show less', 'fluent-cart'), | |
| 236 | + 'load_error' => __('Unable to load reviews. Please try again later.', 'fluent-cart'), | |
| 237 | + 'edit_review' => __('Edit your review', 'fluent-cart'), | |
| 238 | + 'rated_out_of' => __('Rated %d out of 5', 'fluent-cart'), | |
| 239 | + /* translators: 1: current step, 2: total steps */ | |
| 240 | + 'step_x_of_y' => __('Step %1$s of %2$s', 'fluent-cart'), | |
| 241 | + 'rating_poor' => __('Poor', 'fluent-cart'), | |
| 242 | + 'rating_average' => __('Average', 'fluent-cart'), | |
| 243 | + 'rating_good' => __('Good', 'fluent-cart'), | |
| 244 | + 'rating_very_good' => __('Very Good', 'fluent-cart'), | |
| 245 | + 'rating_excellent' => __('Excellent', 'fluent-cart'), | |
| 246 | + 'rating_step_hint' => __('Rate this product', 'fluent-cart'), | |
| 247 | + 'details_step_hint' => __('Tell us more', 'fluent-cart'), | |
| 248 | + 'photos_step_hint' => __('Photos are optional', 'fluent-cart'), | |
| 249 | + ], | |
| 250 | + ]; | |
| 251 | + | |
| 252 | + return apply_filters('fluent_cart/review/js_vars', $reviewVars); | |
| 253 | + } | |
| 254 | + | |
| 110 | 255 | public static function loadSingleProductAssets() |
| 111 | 256 | { |
| 112 | 257 | self::markFrontendAssetsRequired(); |
| 113 | 258 | static $isLoaded = false; |
| @@ -125,11 +270,26 @@ | ||
| 125 | 270 | [ |
| 126 | 271 | 'source' => 'public/single-product/SingleProduct.js', |
| 127 | 272 | 'dependencies' => [], |
| 128 | 273 | 'inFooter' => true |
| 274 | + ], | |
| 275 | + [ | |
| 276 | + 'source' => 'public/single-product/Reviews.js', | |
| 277 | + 'dependencies' => [], | |
| 278 | + 'inFooter' => true | |
| 279 | + ], | |
| 280 | + [ | |
| 281 | + 'source' => 'public/single-product/ReviewForm.js', | |
| 282 | + 'dependencies' => [], | |
| 283 | + 'inFooter' => true, | |
| 284 | + // Named, not numbered, so loadReviewSubmissionFormAssets() enqueueing | |
| 285 | + // the same file lands on the same handle and WordPress drops | |
| 286 | + // the second one. | |
| 287 | + 'handle' => self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 129 | 288 | ] |
| 130 | 289 | ]; |
| 131 | 290 | $localizeData = [ |
| 291 | + 'fluentcart_review_vars' => static::reviewJsVars(), | |
| 132 | 292 | 'fluentcart_single_product_vars' => [ |
| 133 | 293 | 'trans' => TransStrings::singleProductPageString(), |
| 134 | 294 | 'cart_button_text' => apply_filters('fluent_cart/product/add_to_cart_text', __('Add To Cart', 'fluent-cart'), []), |
| 135 | 295 | // App::storeSettings()->get('cart_button_text', __('Add to Cart', 'fluent-cart')), |
| @@ -146,8 +306,9 @@ | ||
| 146 | 306 | |
| 147 | 307 | $singlePageStyles = [ |
| 148 | 308 | 'public/single-product/single-product.scss', |
| 149 | 309 | 'public/single-product/similar-product.scss', |
| 310 | + 'public/single-product/reviews.scss', | |
| 150 | 311 | 'public/product-card/style/product-card.scss', |
| 151 | 312 | 'public/single-product/xzoom/xzoom.css', |
| 152 | 313 | 'public/buttons/add-to-cart/style/style.scss', |
| 153 | 314 | 'public/buttons/direct-checkout/style/style.scss' |
| @@ -342,8 +503,15 @@ | ||
| 342 | 503 | Vite::enqueueStyle('fluentcart-customer-css', |
| 343 | 504 | 'public/customer-profile/style/customer-profile.scss' |
| 344 | 505 | ); |
| 345 | 506 | |
| 507 | + // My Reviews opens the storefront review form in a modal, fetched | |
| 508 | + // per product; the form's own script and stylesheet have to be on | |
| 509 | + // the page before that markup arrives. | |
| 510 | + if (\FluentCart\Api\ModuleSettings::isActive('reviews')) { | |
| 511 | + self::loadReviewSubmissionFormAssets(); | |
| 512 | + } | |
| 513 | + | |
| 346 | 514 | // wp-i18n: this bundle pulls in the admin translator transitively |
| 347 | 515 | // (Start.js -> @/Bits/common.js -> @/utils/translator/Translator.js), |
| 348 | 516 | // which resolves strings through window.wp.i18n. |
| 349 | 517 | Vite::enqueueScript( |
| @@ -411,9 +579,9 @@ | ||
| 411 | 579 | 'rest' => Helper::getRestInfo(), |
| 412 | 580 | 'ajaxurl' => admin_url('admin-ajax.php'), |
| 413 | 581 | ], |
| 414 | 582 | 'fluentcart_drawer_vars' => [ |
| 415 | - 'placeholder_image' => Vite::getAssetUrl('images/placeholder.svg'), | |
| 583 | + 'placeholder_image' => Helper::getProductPlaceholderUrl(), | |
| 416 | 584 | 'cart_item_layout' => $cartItemLayout, |
| 417 | 585 | 'empty_cart_layout' => $emptyCartLayout, |
| 418 | 586 | 'cart_driver' => Helper::getCartDriver(), |
| 419 | 587 | 'cart_image' => Vite::getAssetUrl('images/cart.svg'), |