custom/plugins/theme/src/Resources/views/storefront/component/review/review-modal-inline.html.twig line 1

Open in your IDE?
  1. {% block product_review_modal_inline %}
  2. <!-- Inline CSS for Review Modal -->
  3. <style>
  4. .product-review-modal .star-rating {
  5.     display: flex;
  6.     gap: 4px;
  7.     margin-bottom: 0.5rem;
  8. }
  9. .product-review-modal .star-rating .star {
  10.     font-size: 1.5rem;
  11.     color: #ddd;
  12.     cursor: pointer;
  13.     transition: color 0.2s ease;
  14.     user-select: none;
  15. }
  16. .product-review-modal .star-rating .star:hover,
  17. .product-review-modal .star-rating .star.active {
  18.     color: #ffc107;
  19. }
  20. .product-review-modal .modal-content {
  21.     border-radius: 0.75rem;
  22.     border: none;
  23.     box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  24. }
  25. .product-review-modal .modal-header {
  26.     background: linear-gradient(135deg, var(--bs-primary), #0056b3);
  27.     color: white;
  28.     border-radius: 0.75rem 0.75rem 0 0;
  29.     border-bottom: none;
  30. }
  31. .product-review-modal .modal-header .modal-title {
  32.     font-weight: 600;
  33.     font-size: 1.25rem;
  34.     text-transform: uppercase;
  35. }
  36. .product-review-modal .modal-header .btn-close {
  37.     filter: invert(1);
  38.     opacity: 0.8;
  39. }
  40. .product-review-modal .modal-header .btn-close:hover {
  41.     opacity: 1;
  42. }
  43. .product-review-modal .modal-body {
  44.     padding: 2rem;
  45. }
  46. .product-review-modal .review-step {
  47.     min-height: 200px;
  48.     display: flex;
  49.     flex-direction: column;
  50.     justify-content: center;
  51. }
  52. .product-review-modal .review-step.active {
  53.     animation: fadeIn 0.3s ease-in;
  54. }
  55. .product-review-modal .review-step h3,
  56. .product-review-modal .review-step h6 {
  57.     font-weight: 600;
  58.     color: var(--bs-dark);
  59.     text-transform: uppercase;
  60. }
  61. .product-review-modal #review-step-satisfaction {
  62.     text-align: center;
  63. }
  64. .product-review-modal #review-step-satisfaction .btn {
  65.     min-width: 150px;
  66.     font-weight: 500;
  67.     border-radius: 2rem;
  68.     padding: 0.75rem 2rem;
  69.     transition: all 0.3s ease;
  70. }
  71. .product-review-modal #review-step-satisfaction .btn:hover {
  72.     transform: translateY(-2px);
  73.     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  74. }
  75. .product-review-modal .form-label {
  76.     font-weight: 600;
  77.     color: var(--bs-dark);
  78.     margin-bottom: 0.5rem;
  79. }
  80. .product-review-modal .form-control {
  81.     border-radius: 0.5rem;
  82.     border: 2px solid #e9ecef;
  83.     transition: border-color 0.3s ease;
  84. }
  85. .product-review-modal .form-control:focus {
  86.     border-color: var(--bs-primary);
  87.     box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
  88. }
  89. .product-review-modal .form-control.is-valid {
  90.     border-color: var(--bs-success);
  91. }
  92. .product-review-modal .form-control.is-invalid {
  93.     border-color: var(--bs-danger);
  94. }
  95. .product-review-modal #review-step-success,
  96. .product-review-modal #review-step-error {
  97.     text-align: center;
  98.     min-height: 250px;
  99. }
  100. .product-review-modal .review-footer {
  101.     width: 100%;
  102.     display: flex;
  103.     justify-content: space-between;
  104.     align-items: center;
  105. }
  106. .product-review-modal #footer-satisfaction {
  107.     justify-content: center;
  108. }
  109. @keyframes fadeIn {
  110.     from { opacity: 0; transform: translateY(10px); }
  111.     to { opacity: 1; transform: translateY(0); }
  112. }
  113. @media (max-width: 768px) {
  114.     .product-review-modal .modal-body {
  115.         padding: 1.5rem;
  116.     }
  117.     .product-review-modal #review-step-satisfaction .btn {
  118.         width: 100%;
  119.         margin-bottom: 0.5rem;
  120.     }
  121.     .product-review-modal .star-rating {
  122.         justify-content: center;
  123.     }
  124.     .product-review-modal .star-rating .star {
  125.         font-size: 2rem;
  126.     }
  127. }
  128. </style>
  129. <div class="modal fade product-review-modal" id="productReviewModal" tabindex="-1" aria-labelledby="productReviewModalLabel" aria-hidden="true">
  130.     <div class="modal-dialog modal-lg">
  131.         <div class="modal-content">
  132.             <div class="modal-header">
  133.                 <h5 class="modal-title" id="productReviewModalLabel">
  134.                     {{ "review.modal.title"|trans|sw_sanitize }}
  135.                 </h5>
  136.             </div>
  137.             <div class="modal-body">
  138.                 <!-- Step 1: Satisfaction Question -->
  139.                 <div id="review-step-satisfaction" class="review-step active">
  140.                     <h3 class="mb-4 text-center">{{ "review.modal.satisfaction.title"|trans|sw_sanitize }}</h3>
  141.                     <p class="text-muted mb-4 text-center">{{ "review.modal.satisfaction.subtitle"|trans|sw_sanitize }}</p>
  142.                     <div class="d-flex justify-content-center gap-4">
  143.                         <button type="button" class="btn btn-success btn-lg px-5" id="satisfaction-yes">
  144.                             <i class="fas fa-thumbs-up me-2"></i>
  145.                             {{ "review.modal.satisfaction.yes"|trans|sw_sanitize }}
  146.                         </button>
  147.                         <button type="button" class="btn btn-outline-danger btn-lg px-5" id="satisfaction-no">
  148.                             <i class="fas fa-thumbs-down me-2"></i>
  149.                             {{ "review.modal.satisfaction.no"|trans|sw_sanitize }}
  150.                         </button>
  151.                     </div>
  152.                 </div>
  153.                 <!-- Step 2: Review Form -->
  154.                 <div id="review-step-form" class="review-step d-none">
  155.                     <h6 class="mb-4">{{ "review.modal.form.title"|trans|sw_sanitize }}</h6>
  156.                     <form id="reviewForm" novalidate>
  157.                         <input type="hidden" id="productId" name="productId" value="{{ page.product.id }}">
  158.                         <input type="hidden" id="productNumber" name="productNumber" value="{{ page.product.productNumber }}">
  159.                         <!-- Star Rating -->
  160.                         <div class="mb-4">
  161.                             <label class="form-label">{{ "review.modal.form.rating"|trans|sw_sanitize }} *</label>
  162.                             <div class="star-rating" id="starRating">
  163.                                 <span class="star" data-rating="1">★</span>
  164.                                 <span class="star" data-rating="2">★</span>
  165.                                 <span class="star" data-rating="3">★</span>
  166.                                 <span class="star" data-rating="4">★</span>
  167.                                 <span class="star" data-rating="5">★</span>
  168.                             </div>
  169.                             <input type="hidden" id="rating" name="rating" required>
  170.                             <div class="invalid-feedback">{{ "review.modal.form.rating.required"|trans|sw_sanitize }}</div>
  171.                         </div>
  172.                         <!-- First Name and Last Name in one row -->
  173.                         <div class="row mb-3">
  174.                             <div class="col-md-6">
  175.                                 <label for="reviewerFirstName" class="form-label">{{ "review.modal.form.firstName"|trans|sw_sanitize }} *</label>
  176.                                 <input type="text" class="form-control" id="reviewerFirstName" name="reviewerFirstName" required maxlength="50">
  177.                                 <div class="invalid-feedback">{{ "review.modal.form.firstName.required"|trans|sw_sanitize }}</div>
  178.                             </div>
  179.                             <div class="col-md-6">
  180.                                 <label for="reviewerLastName" class="form-label">{{ "review.modal.form.lastName"|trans|sw_sanitize }} *</label>
  181.                                 <input type="text" class="form-control" id="reviewerLastName" name="reviewerLastName" required maxlength="50">
  182.                                 <div class="invalid-feedback">{{ "review.modal.form.lastName.required"|trans|sw_sanitize }}</div>
  183.                             </div>
  184.                         </div>
  185.                         <!-- Email -->
  186.                         <div class="mb-3">
  187.                             <label for="reviewerEmail" class="form-label">{{ "review.modal.form.email"|trans|sw_sanitize }} *</label>
  188.                             <input type="email" class="form-control" id="reviewerEmail" name="reviewerEmail" required>
  189.                             <div class="invalid-feedback">{{ "review.modal.form.email.required"|trans|sw_sanitize }}</div>
  190.                         </div>
  191.                         <!-- Order Number -->
  192.                         <div class="mb-3">
  193.                             <label for="orderNumber" class="form-label">{{ "review.modal.form.orderNumber"|trans|sw_sanitize }} *</label>
  194.                             <input type="text" class="form-control" id="orderNumber" name="orderNumber" required>
  195.                             <div class="invalid-feedback">{{ "review.modal.form.orderNumber.required"|trans|sw_sanitize }}</div>
  196.                         </div>
  197.                         <!-- Comment -->
  198.                         <div class="mb-4">
  199.                             <label for="reviewComment" class="form-label">{{ "review.modal.form.comment"|trans|sw_sanitize }}</label>
  200.                             <textarea class="form-control" id="reviewComment" name="reviewComment" rows="4" maxlength="500" placeholder="{{ "review.modal.form.comment.placeholder"|trans|sw_sanitize }}"></textarea>
  201.                             <div class="form-text" id="commentCounter">0/500 Zeichen</div>
  202.                         </div>
  203.                         <!-- Privacy Notice -->
  204.                         <div class="mb-4">
  205.                             <div class="form-check">
  206.                                 <input class="form-check-input" type="checkbox" id="privacyAccept" name="privacyAccept" required>
  207.                                 <label class="form-check-label" for="privacyAccept">
  208.                                     {{ "review.modal.form.privacy"|trans|sw_sanitize|raw }}
  209.                                 </label>
  210.                                 <div class="invalid-feedback">{{ "review.modal.form.privacy.required"|trans|sw_sanitize }}</div>
  211.                             </div>
  212.                         </div>
  213.                     </form>
  214.                 </div>
  215.                 <!-- Success Message -->
  216.                 <div id="review-step-success" class="review-step d-none text-center">
  217.                     <div class="text-success mb-3">
  218.                         <i class="fas fa-check-circle fa-3x"></i>
  219.                     </div>
  220.                     <h6 class="mb-3">{{ "review.modal.success.title"|trans|sw_sanitize }}</h6>
  221.                     <p class="text-muted">{{ "review.modal.success.message"|trans|sw_sanitize }}</p>
  222.                 </div>
  223.                 <!-- Error Message -->
  224.                 <div id="review-step-error" class="review-step d-none text-center">
  225.                     <div class="text-danger mb-3">
  226.                         <i class="fas fa-exclamation-circle fa-3x"></i>
  227.                     </div>
  228.                     <h6 class="mb-3">{{ "review.modal.error.title"|trans|sw_sanitize }}</h6>
  229.                     <p class="text-muted" id="error-message">{{ "review.modal.error.message"|trans|sw_sanitize }}</p>
  230.                 </div>
  231.             </div>
  232.             <div class="modal-footer">
  233.                 <div id="footer-satisfaction" class="review-footer active">
  234.                     <!-- No buttons needed for satisfaction step -->
  235.                 </div>
  236.                 <div id="footer-form" class="review-footer d-none">
  237.                     <button type="button" class="btn btn-secondary" id="backToSatisfaction">
  238.                         {{ "review.modal.button.back"|trans|sw_sanitize }}
  239.                     </button>
  240.                     <button type="button" class="btn btn-primary" id="submitReview">
  241.                         <span class="submit-text">{{ "review.modal.button.submit"|trans|sw_sanitize }}</span>
  242.                         <span class="spinner-border spinner-border-sm d-none" role="status">
  243.                             <span class="visually-hidden">Loading...</span>
  244.                         </span>
  245.                     </button>
  246.                 </div>
  247.                 <div id="footer-success" class="review-footer d-none">
  248.                     <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
  249.                         {{ "review.modal.button.close"|trans|sw_sanitize }}
  250.                     </button>
  251.                 </div>
  252.                 <div id="footer-error" class="review-footer d-none">
  253.                     <button type="button" class="btn btn-secondary" id="tryAgain">
  254.                         {{ "review.modal.button.tryAgain"|trans|sw_sanitize }}
  255.                     </button>
  256.                     <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
  257.                         {{ "review.modal.button.cancel"|trans|sw_sanitize }}
  258.                     </button>
  259.                 </div>
  260.             </div>
  261.         </div>
  262.     </div>
  263. </div>
  264. <!-- Inline JavaScript for Review Modal -->
  265. <script>
  266. document.addEventListener('DOMContentLoaded', function() {
  267.     const modal = document.querySelector('#productReviewModal');
  268.     const form = document.querySelector('#reviewForm');
  269.     let currentStep = 'satisfaction';
  270.     let selectedRating = 0;
  271.     // Event Listeners
  272.     const satisfactionYes = document.getElementById('satisfaction-yes');
  273.     const satisfactionNo = document.getElementById('satisfaction-no');
  274.     const backButton = document.getElementById('backToSatisfaction');
  275.     const submitButton = document.getElementById('submitReview');
  276.     const tryAgainButton = document.getElementById('tryAgain');
  277.     satisfactionYes?.addEventListener('click', () => showStep('form'));
  278.     satisfactionNo?.addEventListener('click', handleSatisfactionNo);
  279.     backButton?.addEventListener('click', () => showStep('satisfaction'));
  280.     submitButton?.addEventListener('click', handleSubmitReview);
  281.     tryAgainButton?.addEventListener('click', () => showStep('form'));
  282.     // Star Rating
  283.     const starContainer = document.querySelector('#starRating');
  284.     const stars = starContainer?.querySelectorAll('.star');
  285.     stars?.forEach((star, index) => {
  286.         star.addEventListener('click', () => setRating(index + 1));
  287.         star.addEventListener('mouseenter', () => highlightStars(index + 1));
  288.     });
  289.     starContainer?.addEventListener('mouseleave', () => highlightStars(selectedRating));
  290.     // Comment Counter
  291.     const commentTextarea = document.getElementById('reviewComment');
  292.     const counter = document.getElementById('commentCounter');
  293.     commentTextarea?.addEventListener('input', () => {
  294.         const currentLength = commentTextarea.value.length;
  295.         counter.textContent = `${currentLength}/500 Zeichen`;
  296.     });
  297.     // Form validation
  298.     form?.addEventListener('input', (e) => {
  299.         const input = e.target;
  300.         if (input.checkValidity()) {
  301.             input.classList.remove('is-invalid');
  302.             input.classList.add('is-valid');
  303.         } else {
  304.             input.classList.remove('is-valid');
  305.         }
  306.     });
  307.     // Preload Brevo widget when modal opens
  308.     modal?.addEventListener('shown.bs.modal', () => {
  309.         console.log('Modal opened, preloading Brevo widget');
  310.         // Only load if not already loaded
  311.         if (typeof window.BrevoConversations === 'undefined' && !window.brevoLoadingStarted) {
  312.             window.brevoLoadingStarted = true;
  313.             loadBrevoWidget();
  314.         }
  315.     });
  316.     // Modal reset
  317.     modal?.addEventListener('hidden.bs.modal', resetModal);
  318.     function setRating(rating) {
  319.         selectedRating = rating;
  320.         document.getElementById('rating').value = rating;
  321.         highlightStars(rating);
  322.         document.getElementById('rating').classList.remove('is-invalid');
  323.     }
  324.     function highlightStars(rating) {
  325.         stars?.forEach((star, index) => {
  326.             if (index < rating) {
  327.                 star.classList.add('active');
  328.             } else {
  329.                 star.classList.remove('active');
  330.             }
  331.         });
  332.     }
  333.     function showStep(stepName) {
  334.         document.querySelectorAll('.review-step').forEach(step => {
  335.             step.classList.remove('active');
  336.             step.classList.add('d-none');
  337.         });
  338.         document.querySelectorAll('.review-footer').forEach(footer => {
  339.             footer.classList.remove('active');
  340.             footer.classList.add('d-none');
  341.         });
  342.         const targetStep = document.getElementById(`review-step-${stepName}`);
  343.         const targetFooter = document.getElementById(`footer-${stepName}`);
  344.         if (targetStep) {
  345.             targetStep.classList.remove('d-none');
  346.             targetStep.classList.add('active');
  347.         }
  348.         if (targetFooter) {
  349.             targetFooter.classList.remove('d-none');
  350.             targetFooter.classList.add('active');
  351.         }
  352.         currentStep = stepName;
  353.     }
  354.     function handleSatisfactionNo() {
  355.         console.log('handleSatisfactionNo called');
  356.         // Close modal manually (Bootstrap 5 compatible)
  357.         try {
  358.             console.log('Closing modal');
  359.             modal.classList.remove('show');
  360.             modal.style.display = 'none';
  361.             modal.setAttribute('aria-hidden', 'true');
  362.             modal.removeAttribute('aria-modal');
  363.             modal.removeAttribute('role');
  364.             document.body.classList.remove('modal-open');
  365.             document.body.style.overflow = '';
  366.             document.body.style.paddingRight = '';
  367.             const backdrop = document.querySelector('.modal-backdrop');
  368.             if (backdrop) {
  369.                 backdrop.remove();
  370.             }
  371.         } catch (error) {
  372.             console.error('Error closing modal:', error);
  373.         }
  374.         // Load and open Brevo chat (if not already loaded from modal opening)
  375.         setTimeout(() => {
  376.             if (typeof window.BrevoConversations !== 'undefined') {
  377.                 console.log('Brevo already loaded, opening chat directly');
  378.                 openBrevoChat();
  379.             } else {
  380.                 console.log('Brevo not loaded yet, loading now');
  381.                 loadBrevoWidget(() => openBrevoChat());
  382.             }
  383.         }, 400);
  384.     }
  385.     function loadBrevoWidget(callback) {
  386.         console.log('Loading Brevo Conversations widget...');
  387.         // Check if Brevo is already loaded
  388.         if (typeof window.BrevoConversations !== 'undefined') {
  389.             console.log('Brevo already loaded');
  390.             if (callback) callback();
  391.             return;
  392.         }
  393.         // Check if script is already loading
  394.         if (window.brevoLoadingStarted) {
  395.             console.log('Brevo already loading, waiting...');
  396.             // Wait for it to load and then call callback
  397.             const checkInterval = setInterval(() => {
  398.                 if (typeof window.BrevoConversations !== 'undefined') {
  399.                     clearInterval(checkInterval);
  400.                     console.log('Brevo loaded (via waiting)');
  401.                     if (callback) callback();
  402.                 }
  403.             }, 200);
  404.             return;
  405.         }
  406.         window.brevoLoadingStarted = true;
  407.         // Load Brevo script dynamically
  408.         (function(d, w, c) {
  409.             w.BrevoConversationsID = '66755306756cb768ed6aa8bd';
  410.             w[c] = w[c] || function() {
  411.                 (w[c].q = w[c].q || []).push(arguments);
  412.             };
  413.             var s = d.createElement('script');
  414.             s.async = true;
  415.             s.src = 'https://conversations-widget.brevo.com/brevo-conversations.js';
  416.             s.onload = function() {
  417.                 console.log('Brevo script loaded successfully');
  418.                 // Wait a bit for widget to initialize
  419.                 setTimeout(() => {
  420.                     if (callback) callback();
  421.                 }, 500);
  422.             };
  423.             s.onerror = function() {
  424.                 console.error('Failed to load Brevo script');
  425.                 if (callback) {
  426.                     // Redirect to contact page if we were trying to open chat
  427.                     window.location.href = '/kontakt/';
  428.                 }
  429.             };
  430.             if (d.head) d.head.appendChild(s);
  431.         })(document, window, 'BrevoConversations');
  432.     }
  433.     function openBrevoChat(attempt = 0) {
  434.         console.log('Attempting to open Brevo chat (attempt ' + (attempt + 1) + ')');
  435.         // Method 1: Try Brevo API first (most reliable for opening)
  436.         if (typeof window.BrevoConversations === 'function') {
  437.             console.log('Trying Brevo API');
  438.             try {
  439.                 window.BrevoConversations('openChat', true);
  440.                 console.log('Successfully called openChat');
  441.                 // Wait longer for widget to fully open and initialize before sending message
  442.                 setTimeout(() => {
  443.                     try {
  444.                         // Set the message in the input field
  445.                         window.BrevoConversations('setUserMessage', 'Ich hatte ein Problem mit einem Produkt und benötige Unterstützung.');
  446.                         console.log('Successfully set user message');
  447.                     } catch (e) {
  448.                         console.log('setUserMessage not available, trying alternative:', e);
  449.                         // Alternative: try to find and fill the textarea directly
  450.                         try {
  451.                             const iframe = document.querySelector('iframe[title*="brevo" i]');
  452.                             if (iframe && iframe.contentWindow) {
  453.                                 const textarea = iframe.contentWindow.document.querySelector('textarea');
  454.                                 if (textarea) {
  455.                                     textarea.value = 'Ich hatte ein Problem mit einem Produkt und benötige Unterstützung.';
  456.                                     console.log('Filled textarea directly');
  457.                                 }
  458.                             }
  459.                         } catch (iframeError) {
  460.                             console.log('Cannot access iframe:', iframeError);
  461.                         }
  462.                     }
  463.                 }, 1500);
  464.                 return;
  465.             } catch (error) {
  466.                 console.error('Brevo API error:', error);
  467.             }
  468.         }
  469.         // Method 2: Try to find and click the launcher button
  470.         const launcherSelectors = [
  471.             '.brevo-conversations-launcher',
  472.             'button[class*="brevo"]',
  473.             'button[class*="launcher"]',
  474.             'div[class*="brevo"][class*="launcher"]',
  475.             '[role="button"][class*="brevo"]'
  476.         ];
  477.         for (const selector of launcherSelectors) {
  478.             const launcher = document.querySelector(selector);
  479.             if (launcher) {
  480.                 console.log('Found launcher button:', selector, launcher);
  481.                 launcher.click();
  482.                 return;
  483.             }
  484.         }
  485.         // Method 3: Look for iframe and find launcher nearby
  486.         const iframe = document.querySelector('iframe[title*="brevo" i]') ||
  487.                        document.querySelector('iframe[title*="chat" i]');
  488.         if (iframe) {
  489.             console.log('Found Brevo iframe, searching for launcher');
  490.             // Search in parent and siblings
  491.             const parent = iframe.parentElement;
  492.             if (parent) {
  493.                 // Look for siblings that might be the launcher
  494.                 const siblings = parent.parentElement?.children;
  495.                 if (siblings) {
  496.                     for (const sibling of siblings) {
  497.                         if (sibling !== parent &&
  498.                             (sibling.className.includes('launcher') ||
  499.                              sibling.className.includes('button'))) {
  500.                             console.log('Found launcher sibling:', sibling);
  501.                             sibling.click();
  502.                             return;
  503.                         }
  504.                     }
  505.                 }
  506.             }
  507.         }
  508.         console.log('BrevoConversations type:', typeof window.BrevoConversations);
  509.         // Method 4: Retry up to 10 times (wait for Brevo to load and initialize)
  510.         if (attempt < 10) {
  511.             console.log('Brevo not ready, retrying in 500ms...');
  512.             setTimeout(() => openBrevoChat(attempt + 1), 500);
  513.         } else {
  514.             console.error('Failed to open Brevo chat after 10 attempts, redirecting to contact page');
  515.             window.location.href = '/kontakt/';
  516.         }
  517.     }
  518.     async function handleSubmitReview(e) {
  519.         e.preventDefault();
  520.         if (!validateForm()) {
  521.             return;
  522.         }
  523.         const submitText = submitButton.querySelector('.submit-text');
  524.         const spinner = submitButton.querySelector('.spinner-border');
  525.         submitButton.disabled = true;
  526.         submitText.classList.add('d-none');
  527.         spinner.classList.remove('d-none');
  528.         try {
  529.             const formData = new FormData(form);
  530.             const data = Object.fromEntries(formData.entries());
  531.             // Combine first and last name into reviewerName field
  532.             const firstName = data.reviewerFirstName || '';
  533.             const lastName = data.reviewerLastName || '';
  534.             data.reviewerName = (firstName + ' ' + lastName).trim();
  535.             // Remove separate fields as they're not needed by the backend
  536.             delete data.reviewerFirstName;
  537.             delete data.reviewerLastName;
  538.             const response = await fetch('/product-review/submit', {
  539.                 method: 'POST',
  540.                 headers: {
  541.                     'Content-Type': 'application/json'
  542.                 },
  543.                 body: JSON.stringify(data)
  544.             });
  545.             const result = await response.json();
  546.             if (result.success) {
  547.                 showStep('success');
  548.             } else {
  549.                 showError(result.message || 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
  550.             }
  551.         } catch (error) {
  552.             console.error('Review submission error:', error);
  553.             showError('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
  554.         } finally {
  555.             submitButton.disabled = false;
  556.             submitText.classList.remove('d-none');
  557.             spinner.classList.add('d-none');
  558.         }
  559.     }
  560.     function validateForm() {
  561.         let isValid = true;
  562.         if (selectedRating === 0) {
  563.             document.getElementById('rating').classList.add('is-invalid');
  564.             isValid = false;
  565.         }
  566.         const requiredFields = form?.querySelectorAll('[required]');
  567.         requiredFields?.forEach(field => {
  568.             if (!field.checkValidity()) {
  569.                 field.classList.add('is-invalid');
  570.                 isValid = false;
  571.             }
  572.         });
  573.         if (!isValid) {
  574.             const firstInvalid = form?.querySelector('.is-invalid');
  575.             firstInvalid?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  576.         }
  577.         return isValid;
  578.     }
  579.     function showError(message) {
  580.         document.getElementById('error-message').textContent = message;
  581.         showStep('error');
  582.     }
  583.     function resetModal() {
  584.         form?.reset();
  585.         form?.querySelectorAll('.is-valid, .is-invalid').forEach(element => {
  586.             element.classList.remove('is-valid', 'is-invalid');
  587.         });
  588.         selectedRating = 0;
  589.         highlightStars(0);
  590.         document.getElementById('rating').value = '';
  591.         const counter = document.querySelector('.form-text');
  592.         if (counter) {
  593.             counter.textContent = '0/500 Zeichen';
  594.         }
  595.         showStep('satisfaction');
  596.     }
  597. });
  598. </script>
  599. {% endblock %}