app/template/default/Product/detail.twig line 1

Open in your IDE?
  1. {#
  2. This file is part of EC-CUBE
  3. Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  4. http://www.ec-cube.co.jp/
  5. For the full copyright and license information, please view the LICENSE
  6. file that was distributed with this source code.
  7. #}
  8. {% extends 'default_frame.twig' %}
  9. {% set isHasLeftSide = false %}
  10. {% block stylesheet %}
  11.     <link rel="stylesheet" href="{{ asset('assets/css/product-detail.css') }}">
  12.     <link rel="stylesheet" href="{{ asset('assets/CustomComponent/modal.css') }}">
  13.     <style>
  14.         .custom-quantity {
  15.             border: none;
  16.             font-size: 18px;
  17.             text-align: center;
  18.         }
  19.         /* remove number input arrow */
  20.         .custom-quantity::-webkit-outer-spin-button,
  21.         .custom-quantity::-webkit-inner-spin-button {
  22.             -webkit-appearance: none;
  23.             margin: 0;
  24.         }
  25.         .custom-quantity[type=number] {
  26.             -moz-appearance: textfield;
  27.         }
  28.         .custom-quantity[type=number] {
  29.             -moz-appearance: textfield;
  30.         }
  31.         .slick-slider {
  32.             margin-bottom: 10px;
  33.         }
  34.         .slick-dots {
  35.             position: absolute;
  36.             bottom: -45px;
  37.             display: block;
  38.             width: 100%;
  39.             padding: 0;
  40.             list-style: none;
  41.             text-align: center;
  42.         }
  43.         .slick-dots li {
  44.             position: relative;
  45.             display: inline-block;
  46.             width: 20px;
  47.             height: 20px;
  48.             margin: 0 5px;
  49.             padding: 0;
  50.             cursor: pointer;
  51.         }
  52.         .slick-dots li button {
  53.             font-size: 0;
  54.             line-height: 0;
  55.             display: block;
  56.             width: 20px;
  57.             height: 20px;
  58.             padding: 5px;
  59.             cursor: pointer;
  60.             color: transparent;
  61.             border: 0;
  62.             outline: none;
  63.             background: transparent;
  64.         }
  65.         .slick-dots li button:hover,
  66.         .slick-dots li button:focus {
  67.             outline: none;
  68.         }
  69.         .slick-dots li button:hover:before,
  70.         .slick-dots li button:focus:before {
  71.             opacity: 1;
  72.         }
  73.         .slick-dots li button:before {
  74.             content: " ";
  75.             line-height: 20px;
  76.             position: absolute;
  77.             top: 0;
  78.             left: 0;
  79.             width: 12px;
  80.             height: 12px;
  81.             text-align: center;
  82.             opacity: .25;
  83.             background-color: black;
  84.             border-radius: 50%;
  85.         }
  86.         .slick-dots li.slick-active button:before {
  87.             opacity: .75;
  88.             background-color: black;
  89.         }
  90.         .slick-dots li button.thumbnail img {
  91.             width: 0;
  92.             height: 0;
  93.         }
  94.         .slide-item img {
  95.             border-radius: 10px;
  96.             object-fit: contain;
  97.             max-height: 550px;
  98.             height: 550px;
  99.             width: 100%;
  100.             background-color: #E9E9E9;
  101.         }
  102.         .ec-sliderItemRole .slideThumb img {
  103.             width: 100%;
  104.             border-radius: 6px;
  105.             height: 133px;
  106.             max-height: 133px;
  107.             object-fit: contain;
  108.         }
  109.         .ec-sliderItemRole .slideThumb {
  110.             margin-bottom: 0;
  111.             width: calc(25% - 7.5px);
  112.         }
  113.         .ec-sliderItemRole .item_nav {
  114.             gap: 10px;
  115.         }
  116.         .item_nav {
  117.             display: flex;
  118.             gap: 16px;
  119.             flex-wrap: wrap;
  120.         }
  121.         .slideThumb {
  122.             width: calc(25% - 12px);
  123.         }
  124.         @media screen and (max-width: 1200px) {
  125.             .item_nav {
  126.                 gap: 10px;
  127.             }
  128.             .slideThumb {
  129.                 width: calc(25% - 7.5px);
  130.                 height: 20vw;
  131.             }
  132.             .slide-item img {
  133.                 height: auto;
  134.                 max-height: 500px;
  135.             }
  136.         }
  137.         @media screen and (max-width: 768px) {
  138.             .slide-item img {
  139.                 height: 342px;
  140.             }
  141.         }
  142.         .ec-grid2 {
  143.             gap: 10px;
  144.         }
  145.         .main_button_add_cart, .main_button_add_cart:hover {
  146.             background-color: var(--cart-button-color);
  147.         }
  148.     </style>
  149.     {% if form.user_imei is defined %}
  150.     <style>
  151.         .user-imei-form .ec-errorMessage {
  152.             color: #EF2864;
  153.             font-family: 'Noto Sans JP', sans-serif;
  154.             font-weight: 500;
  155.             font-size: 15px;
  156.             line-height: 1.8em;
  157.             margin-top: 8px;
  158.             margin-bottom: 0;
  159.         }
  160.         .user-imei-form.error #user_imei,
  161.         .user-imei-form #user_imei.customer-input-error {
  162.             border-color: #EF2864;
  163.             background-color: #FDF1F0;
  164.         }
  165.     </style>
  166.     {% endif %}
  167. {% endblock %}
  168. {% set body_class = 'product_page' %}
  169. {# script #}
  170. {% block javascript %}
  171. {% if form.user_imei is defined %}
  172. <script src="{{ asset('assets/js/customer-input-validate.js') }}"></script>
  173. {% endif %}
  174. <script>
  175. $(document).ready(function() {
  176.     $('.item-minus').click(function () {
  177.         // Check if button is disabled
  178.         if ($(this).hasClass('btn-disabled')) {
  179.             return;
  180.         }
  181.         const $input = $(this).siblings('.quantity-value').find('.custom-quantity');
  182.         let current = parseInt($input.val()) || 0;
  183.         if (current > 1) {
  184.             $input.val(current - 1);
  185.         }
  186.     });
  187.     $('.item-plus').click(function () {
  188.         // Check if button is disabled
  189.         if ($(this).hasClass('btn-disabled')) {
  190.             return;
  191.         }
  192.         const $input = $(this).siblings('.quantity-value').find('.custom-quantity');
  193.         let current = parseInt($input.val()) || 0;
  194.         $input.val(current + 1);
  195.     });
  196. });
  197. const params = new URLSearchParams(window.location.search);
  198. const imei = params.get('imei');
  199. if (imei) {
  200.     $('#user_imei').val(imei);
  201. }
  202. {% if form.user_imei is defined %}
  203. $(function() {
  204.     var $imei = $('#user_imei');
  205.     if (!$imei.length || !window.CustomerInputValidate) {
  206.         return;
  207.     }
  208.     var imeiRequiredMsg = '{{ 'front.cart.user_imei_required'|trans }}';
  209.     $imei.on('blur.imeiValidate', function() {
  210.         if (!$.trim($(this).val() || '')) {
  211.             CustomerInputValidate.showInlineError($imei, imeiRequiredMsg, '.user-imei-form');
  212.         } else {
  213.             CustomerInputValidate.clearInlineError($imei, '.user-imei-form');
  214.         }
  215.     });
  216.     $imei.on('input.imeiValidate', function() {
  217.         if ($.trim($(this).val() || '')) {
  218.             CustomerInputValidate.clearInlineError($imei, '.user-imei-form');
  219.         }
  220.     });
  221. });
  222. {% endif %}
  223. </script>
  224. <script>
  225.     eccube.classCategories = {{ class_categories_as_json(Product)|raw }};
  226.     // 規格2に選択肢を割り当てる。
  227.     function fnSetClassCategories(form, classcat_id2_selected) {
  228.         var $form = $(form);
  229.         var product_id = $form.find('input[name=product_id]').val();
  230.         var $sele1 = $form.find('select[name=classcategory_id1]');
  231.         var $sele2 = $form.find('select[name=classcategory_id2]');
  232.         eccube.setClassCategories($form, product_id, $sele1, $sele2, classcat_id2_selected);
  233.     }
  234.     {% if form.classcategory_id2 is defined %}
  235.     fnSetClassCategories(
  236.         $('#form1'), {{ form.classcategory_id2.vars.value|json_encode|raw }}
  237.     );
  238.     {% elseif form.classcategory_id1 is defined %}
  239.     eccube.checkStock($('#form1'), {{ Product.id }}, {{ form.classcategory_id1.vars.value|json_encode|raw }}, null);
  240.     {% endif %}
  241.     // Thêm thuộc tính is_country_category cho classcategory select
  242.     function addCountryCategoryAttributes() {
  243.         // Lấy country_id từ URL
  244.         const countryId = params.get('country_id');
  245.         console.log('Country ID from URL:', countryId);
  246.         console.log('eccube.classCategories:', eccube.classCategories);
  247.         if (!countryId) {
  248.             console.log('No country_id found in URL');
  249.             return; // Không có country_id thì không làm gì
  250.         }
  251.         let hasSelected = false;
  252.         // Thêm thuộc tính cho classcategory_id1
  253.         const $select1 = $('select[name=classcategory_id1]');
  254.         if ($select1.length) {
  255.             console.log('Found select1, checking options...');
  256.             $select1.find('option').each(function() {
  257.                 const $option = $(this);
  258.                 const optionValue = $option.val();
  259.                 let isCountryCategoryFlag = false;
  260.                 if (optionValue && optionValue !== '__unselected') {
  261.                     isCountryCategoryFlag = isCountryCategory(optionValue);
  262.                     if (isCountryCategoryFlag && optionValue === countryId) {
  263.                         console.log('Auto-selecting country category:', optionValue, 'in select1');
  264.                         $option.prop('selected', true);
  265.                         hasSelected = true;
  266.                     }
  267.                 }
  268.                 $option.attr('data-is-country-category', isCountryCategoryFlag);
  269.             });
  270.             // Trigger change event sau khi đã chọn tất cả options
  271.             if (hasSelected) {
  272.                 setTimeout(function() {
  273.                     $select1.trigger('change');
  274.                 }, 10);
  275.             }
  276.         }
  277.         // Thêm thuộc tính cho classcategory_id2
  278.         const $select2 = $('select[name=classcategory_id2]');
  279.         if ($select2.length) {
  280.             console.log('Found select2, checking options...');
  281.             $select2.find('option').each(function() {
  282.                 const $option = $(this);
  283.                 const optionValue = $option.val();
  284.                 // Kiểm tra xem option này có phải là country category không
  285.                 let isCountryCategoryFlag = false;
  286.                 if (optionValue && optionValue !== '__unselected') {
  287.                     // Kiểm tra xem option này có phải là country category không
  288.                     isCountryCategoryFlag = isCountryCategory(optionValue);
  289.                     // Nếu đây là country category và match với country_id, tự động chọn
  290.                     if (isCountryCategoryFlag && optionValue === countryId) {
  291.                         console.log('Auto-selecting country category:', optionValue, 'in select2');
  292.                         $option.prop('selected', true);
  293.                         hasSelected = true;
  294.                     }
  295.                 }
  296.                 // Thêm thuộc tính data-is-country-category
  297.                 $option.attr('data-is-country-category', isCountryCategoryFlag);
  298.             });
  299.             // Trigger change event sau khi đã chọn tất cả options
  300.             if (hasSelected) {
  301.                 setTimeout(function() {
  302.                     $select2.trigger('change');
  303.                 }, 10);
  304.             }
  305.         }
  306.         if (hasSelected) {
  307.             console.log('Country category auto-selection completed');
  308.         }
  309.     }
  310.     // Function để kiểm tra xem một classcategory có phải là country category không
  311.     function isCountryCategory(categoryId) {
  312.         if (eccube.classCategories && eccube.classCategories[categoryId]) {
  313.             console.log('Category', categoryId, 'found in eccube.classCategories');
  314.             return true;
  315.         }
  316.         return false;
  317.     }
  318.     // Chạy function khi document ready và sau khi các select đã được khởi tạo
  319.     $(document).ready(function() {
  320.         // Delay một chút để đảm bảo các select đã được khởi tạo
  321.         setTimeout(function() {
  322.             addCountryCategoryAttributes();
  323.         }, 100);
  324.     });
  325.     // Custom function to update price display
  326.     function updatePriceDisplay(classcat2) {
  327.         var $price02 = $('.price02-default');
  328.         try {
  329.             if (classcat2 && typeof classcat2.price02_inc_tax !== 'undefined' && String(classcat2.price02_inc_tax).length >= 1) {
  330.                 // Show specific price when category is selected
  331.                 $price02.text(classcat2.price02_inc_tax_with_currency).addClass('selected');
  332.             } else {
  333.                 // Show range price when no category is selected
  334.                 var originalPrice = $price02.attr('data-original-price');
  335.                 if (originalPrice) {
  336.                     $price02.text(originalPrice).removeClass('selected');
  337.                 }
  338.             }
  339.         } catch (e) {
  340.             console.log('Error updating price display:', e);
  341.             // Fallback to original price
  342.             var originalPrice = $price02.attr('data-original-price');
  343.             if (originalPrice) {
  344.                 $price02.text(originalPrice).removeClass('selected');
  345.             }
  346.         }
  347.     }
  348.     // Add event listeners for category changes
  349.     $(document).ready(function() {
  350.         // Listen for category 1 changes
  351.         $('select[name=classcategory_id1]').on('change', function() {
  352.             var $form = $(this).parents('form');
  353.             var product_id = $form.find('input[name=product_id]').val();
  354.             var $sele1 = $(this);
  355.             var $sele2 = $form.find('select[name=classcategory_id2]');
  356.             // Update price display after a short delay to let original function complete
  357.             setTimeout(function() {
  358.                 updatePriceFromCategories(product_id, $sele1.val(), $sele2.val());
  359.             }, 150);
  360.         });
  361.         // Listen for category 2 changes
  362.         $('select[name=classcategory_id2]').on('change', function() {
  363.             var $form = $(this).parents('form');
  364.             var product_id = $form.find('input[name=product_id]').val();
  365.             var $sele1 = $form.find('select[name=classcategory_id1]');
  366.             var $sele2 = $form.find('select[name=classcategory_id2]');
  367.             // Update price display after a short delay to let original function complete
  368.             setTimeout(function() {
  369.                 updatePriceFromCategories(product_id, $sele1.val(), $sele2.val());
  370.             }, 150);
  371.         });
  372.     });
  373.     // Function to update price based on selected categories
  374.     function updatePriceFromCategories(product_id, classcat_id1, classcat_id2) {
  375.         try {
  376.             // Don't update if categories are not properly selected
  377.             if (!classcat_id1 || classcat_id1 === '__unselected' || classcat_id1 === '') {
  378.                 var $price02 = $('.price02-default');
  379.                 var originalPrice = $price02.attr('data-original-price');
  380.                 if (originalPrice) {
  381.                     $price02.text(originalPrice).removeClass('selected');
  382.                 }
  383.                 return;
  384.             }
  385.             // Normalize: khi classcat_id2 là null hoặc __unselected phải dùng '' để lookup đúng key '#'
  386.             var classcat2Key = '#' + (classcat_id2 && classcat_id2 !== '__unselected' ? classcat_id2 : '');
  387.             var classcat2;
  388.             if (eccube.hasOwnProperty('productsClassCategories')) {
  389.                 if (eccube.productsClassCategories[product_id] &&
  390.                     eccube.productsClassCategories[product_id][classcat_id1]) {
  391.                     classcat2 = eccube.productsClassCategories[product_id][classcat_id1][classcat2Key];
  392.                 }
  393.             } else {
  394.                 if (typeof eccube.classCategories[classcat_id1] !== 'undefined') {
  395.                     classcat2 = eccube.classCategories[classcat_id1][classcat2Key];
  396.                 }
  397.             }
  398.             updatePriceDisplay(classcat2);
  399.         } catch (e) {
  400.             console.log('Error updating price from categories:', e);
  401.             // Fallback to original price
  402.             var $price02 = $('.price02-default');
  403.             var originalPrice = $price02.attr('data-original-price');
  404.             if (originalPrice) {
  405.                 $price02.text(originalPrice).removeClass('selected');
  406.             }
  407.         }
  408.     }
  409.     // Initialize price display when page loads
  410.     $(document).ready(function() {
  411.         // Store original price if not already stored
  412.         var $price02 = $('.price02-default');
  413.         if (!$price02.attr('data-original-price')) {
  414.             $price02.attr('data-original-price', $price02.text());
  415.         }
  416.         // Check if any category is selected on page load
  417.         var $classcat1 = $('select[name=classcategory_id1]');
  418.         var $classcat2 = $('select[name=classcategory_id2]');
  419.         // Only update price if categories are properly selected
  420.         if ($classcat1.length && $classcat1.val() && $classcat1.val() !== '__unselected') {
  421.             // Category 1 is selected, trigger price update
  422.             if ($classcat2.length && $classcat2.val() && $classcat2.val() !== '__unselected') {
  423.                 // Both categories are selected
  424.                 setTimeout(function() {
  425.                     updatePriceFromCategories({{ Product.id }}, $classcat1.val(), $classcat2.val());
  426.                 }, 200);
  427.             } else {
  428.                 // Only category 1 is selected
  429.                 setTimeout(function() {
  430.                     updatePriceFromCategories({{ Product.id }}, $classcat1.val(), null);
  431.                 }, 200);
  432.             }
  433.         }
  434.     });
  435. </script>
  436.     <script>
  437.         $(function() {
  438.             // bfcache無効化
  439.             $(window).bind('pageshow', function(event) {
  440.                 if (event.originalEvent.persisted) {
  441.                     location.reload(true);
  442.                 }
  443.             });
  444.             // Core Web Vital の Cumulative Layout Shift(CLS)対策のため
  445.             // img タグに width, height が付与されている.
  446.             // 630px 未満の画面サイズでは縦横比が壊れるための対策
  447.             // see https://github.com/EC-CUBE/ec-cube/pull/5023
  448.             $('.ec-grid2__cell').hide();
  449.             var removeSize = function () {
  450.                 $('.slide-item').height('');
  451.                 $('.slide-item img')
  452.                     .removeAttr('width')
  453.                     .removeAttr('height')
  454.                     .removeAttr('style');
  455.             };
  456.             var slickInitial = function(slick) {
  457.                 $('.ec-grid2__cell').fadeIn(1500);
  458.                 var baseHeight = $(slick.target).height();
  459.                 var baseWidth = $(slick.target).width();
  460.                 var rate = baseWidth / baseHeight;
  461.                 $('.slide-item').height(baseHeight * rate); // 余白を削除する
  462.                 // transform を使用することでCLSの影響を受けないようにする
  463.                 $('.slide-item img')
  464.                     .css(
  465.                         {
  466.                             'transform-origin': 'top left',
  467.                             'transform': 'scaleY(' + rate + ')',
  468.                             'transition': 'transform .1s'
  469.                         }
  470.                     );
  471.                 // 正しいサイズに近くなったら属性を解除する
  472.                 setTimeout(removeSize, 500);
  473.             };
  474.             $('.item_visual').on('init', slickInitial);
  475.             // リサイズ時は CLS の影響を受けないため属性を解除する
  476.             $(window).resize(removeSize);
  477.             $('.item_visual').slick({
  478.                 dots: false,
  479.                 arrows: false
  480.             });
  481.             $('.slideThumb').on('click', function() {
  482.                 var index = $(this).attr('data-index');
  483.                 $('.item_visual').slick('slickGoTo', index, false);
  484.             })
  485.         });
  486.     </script>
  487.     <script>
  488.         $(function() {
  489.             {% set just_added_favorite = app.session.flashbag.get('product_detail.just_added_favorite') %}
  490.             {% if just_added_favorite and Product.id == just_added_favorite[0] %}
  491.                 alert('{{ 'front.product.added_to_favorite'|trans }}');
  492.             {% endif %}
  493.             $('.add-cart').on('click', function(event) {
  494.                 {% if form.classcategory_id1 is defined %}
  495.                     // 規格1フォームの必須チェック
  496.                     if ($('#classcategory_id1').val() == '__unselected' || $('#classcategory_id1').val() == '') {
  497.                         $('#classcategory_id1')[0].setCustomValidity('{{ 'front.product.product_class_unselected'|trans }}');
  498.                         return true;
  499.                     } else {
  500.                         $('#classcategory_id1')[0].setCustomValidity('');
  501.                     }
  502.                 {% endif %}
  503.                 {% if form.classcategory_id2 is defined %}
  504.                     // 規格2フォームの必須チェック
  505.                     if ($('#classcategory_id2').val() == '__unselected' || $('#classcategory_id2').val() == '') {
  506.                         $('#classcategory_id2')[0].setCustomValidity('{{ 'front.product.product_class_unselected'|trans }}');
  507.                         return true;
  508.                     } else {
  509.                         $('#classcategory_id2')[0].setCustomValidity('');
  510.                     }
  511.                 {% endif %}
  512.                 // 個数フォームのチェック
  513.                 if ($('#quantity').val() < 1) {
  514.                     $('#quantity')[0].setCustomValidity('{{ 'front.product.invalid_quantity'|trans }}');
  515.                     return true;
  516.                 } else {
  517.                     $('#quantity')[0].setCustomValidity('');
  518.                 }
  519.                 // IMEIフォームのチェック
  520.                 {% if Product.hasUserImeiCategory %}
  521.                     var $imei = $('#user_imei');
  522.                     var imeiRequiredMsg = '{{ 'front.cart.user_imei_required'|trans }}';
  523.                     if (!$imei.length || !$.trim($imei.val() || '')) {
  524.                         event.preventDefault();
  525.                         if ($imei.length && window.CustomerInputValidate) {
  526.                             CustomerInputValidate.showInlineError($imei, imeiRequiredMsg, '.user-imei-form');
  527.                             CustomerInputValidate.scrollAndFlash($imei, { wrapperSelector: '.user-imei-form' });
  528.                         } else if ($imei.length) {
  529.                             alert(imeiRequiredMsg);
  530.                         } else {
  531.                             alert(imeiRequiredMsg);
  532.                         }
  533.                         return false;
  534.                     } else if ($imei.length && window.CustomerInputValidate) {
  535.                         CustomerInputValidate.clearInlineError($imei, '.user-imei-form');
  536.                     }
  537.                 {% endif %}
  538.                 event.preventDefault();
  539.                 $form = $('#form1');
  540.                 $.ajax({
  541.                     url: $form.attr('action'),
  542.                     type: $form.attr('method'),
  543.                     data: $form.serialize(),
  544.                     dataType: 'json',
  545.                     beforeSend: function(xhr, settings) {
  546.                         // Buttonを無効にする
  547.                         $('.add-cart').prop('disabled', true);
  548.                     }
  549.                 }).done(function(data) {
  550.                     if (data.error_code == 'OUT_OF_STOCK_ZERO' || data.error_code == 'OUT_OF_STOCK') {
  551.                         alert('この商品の在庫は残り' + data.stock_left + 'です。');
  552.                     } else if (data.error_code == 'DUPLICATE_IMEI') {
  553.                         {% if form.user_imei is defined %}
  554.                         if (window.CustomerInputValidate && $('#user_imei').length) {
  555.                             CustomerInputValidate.showInlineError($('#user_imei'), '{{ 'front.shopping.duplicate_imei'|trans }}', '.user-imei-form');
  556.                             CustomerInputValidate.scrollAndFlash($('#user_imei'), { wrapperSelector: '.user-imei-form' });
  557.                         } else {
  558.                             alert('{{ 'front.shopping.duplicate_imei'|trans }}');
  559.                         }
  560.                         {% else %}
  561.                         alert('{{ 'front.shopping.duplicate_imei'|trans }}');
  562.                         {% endif %}
  563.                     } else if (data.error_code == 'CART_HAS_PRODUCT') {
  564.                         alert('{{ 'front.shopping.cart_has_product'|trans }}');
  565.                     } else if (data.error_code == 'IMEI_VALIDATION_FAILED') {
  566.                         {% if form.user_imei is defined %}
  567.                         if (window.CustomerInputValidate && $('#user_imei').length) {
  568.                             CustomerInputValidate.showInlineError($('#user_imei'), '{{ 'front.shopping.imei_validation_failed'|trans }}', '.user-imei-form');
  569.                             CustomerInputValidate.scrollAndFlash($('#user_imei'), { wrapperSelector: '.user-imei-form' });
  570.                         } else {
  571.                             alert('{{ 'front.shopping.imei_validation_failed'|trans }}');
  572.                         }
  573.                         {% else %}
  574.                         alert('{{ 'front.shopping.imei_validation_failed'|trans }}');
  575.                         {% endif %}
  576.                     } else {
  577.                         // カートブロックを更新する
  578.                         $.ajax({
  579.                             url: "{{ url('block_cart') }}",
  580.                             type: 'GET',
  581.                             dataType: 'html'
  582.                         }).done(function(html) {
  583.                             $('.ec-headerRole__cart').html(html);
  584.                         });
  585.                         // Hiển thị popup thông báo thành công sử dụng ModalComponent
  586.                         if (window.ModalUtils) {
  587.                             window.ModalUtils.showSuccess('cart-success-modal',
  588.                                 '{{ 'front.modal.cart_success.title'|trans({}, 'messages') }}',
  589.                                 '{{ 'front.modal.cart_success.message'|trans({}, 'messages') }}'
  590.                             );
  591.                         } else {
  592.                             // Fallback nếu ModalComponent chưa load
  593.                             alert('{{ 'front.modal.cart_success.title'|trans({}, 'messages') }}');
  594.                         }
  595.                     }
  596.                 }).fail(function(data) {
  597.                     alert('{{ 'front.product.add_cart_error'|trans }}');
  598.                 }).always(function(data) {
  599.                     // Buttonを有効にする
  600.                     $('.add-cart').prop('disabled', false);
  601.                 });
  602.             });
  603.         });
  604.         // Modal events are now handled by ModalComponent
  605.     </script>
  606.     <script src="{{ asset('assets/CustomComponent/modal.js') }}"></script>
  607.     </script>
  608.     <script type="application/ld+json">
  609.     {
  610.         "@context": "https://schema.org/",
  611.         "@type": "Product",
  612.         "name": "{{ Product.name }}",
  613.         "image": [
  614.             {% for img in Product.ProductImage %}
  615.                 "{{ app.request.schemeAndHttpHost }}{{ asset(img, 'save_image') }}"{% if not loop.last %},{% endif %}
  616.             {% else %}
  617.                 "{{ app.request.schemeAndHttpHost }}{{ asset(''|no_image_product, 'save_image') }}"
  618.             {% endfor %}
  619.         ],
  620.         "description": "{{ Product.description_list | default(Product.description_detail) | replace({'\n': '', '\r': ''}) | slice(0,300) }}",
  621.         {% if Product.code_min %}
  622.         "sku": "{{ Product.code_min }}",
  623.         {% endif %}
  624.         "offers": {
  625.             "@type": "Offer",
  626.             "url": "{{ url('product_detail', {'id': Product.id}) }}",
  627.             "priceCurrency": "{{ eccube_config.currency }}",
  628.             "price": {{ Product.getPrice02IncTaxMin ? Product.getPrice02IncTaxMin : 0}},
  629.             "availability": "{{ Product.stock_find ? "InStock" : "OutOfStock" }}"
  630.         }
  631.     }
  632.     </script>
  633. {% endblock %}
  634. {% block main %}
  635.     <!-- breadcrumb section -->
  636.     <div class="breadcrumb-section">
  637.         <div class="container-1360 border-box px-40">
  638.             <nav class="breadcrumb">
  639.                 <a href="{{ url('homepage') }}" class="breadcrumb__item text-black">ホーム</a>
  640.                 <img src="{{ asset('assets/img/default/icons/icon-breadcrumb-arrow.svg') }}" alt=">" class="breadcrumb__separator">
  641.                 <a href="{{ url('product_list') }}" class="breadcrumb__item text-black">商品一覧</a>
  642.                 <img src="{{ asset('assets/img/default/icons/icon-breadcrumb-arrow.svg') }}" alt=">" class="breadcrumb__separator">
  643.                 <span class="breadcrumb__item breadcrumb__item--current text-black">{{ Product.name }}</span>
  644.             </nav>
  645.         </div>
  646.     </div>
  647.     <!-- product detail section -->
  648.     <div class="product-detail-section container-1200">
  649.         <div class="product-detail-wrapper">
  650.             <div class="product-detail-left">
  651.                 <!-- Main gallery (using existing slick) -->
  652.                 <div class="item_visual">
  653.                     {% for ProductImage in Product.ProductImage %}
  654.                         <div class="slide-item"><img src="{{ asset(ProductImage, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" width="550" height="550"{% if loop.index > 1 %} loading="lazy"{% endif %}></div>
  655.                     {% else %}
  656.                         <div class="slide-item"><img src="{{ asset(''|no_image_product, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" width="550" height="550"></div>
  657.                     {% endfor %}
  658.                 </div>
  659.                 <!-- Thumbs below -->
  660.                 <div class="product-detail-left-bottom">
  661.                     <div class="item_nav">
  662.                         {% for ProductImage in Product.ProductImage %}
  663.                             <div class="slideThumb" data-index="{{ loop.index0 }}">
  664.                                 <div class="product-thumbnail"><img src="{{ asset(ProductImage, 'save_image') }}" alt="" width="133" height="133" loading="lazy"></div>
  665.                             </div>
  666.                         {% endfor %}
  667.                     </div>
  668.                 </div>
  669.             </div>
  670.             <div class="product-detail-right">
  671.                 <!-- Product Title -->
  672.                 <h1 class="product-title">{{ Product.name }}</h1>
  673.                 <!-- Product Price -->
  674.                 <div class="product-price">
  675.                     {% if Product.hasProductClass -%}
  676.                         <span class="product-price-value price02-default text-color-primary" data-original-price="{{ Product.getPrice02IncTaxMin|price }}{% if Product.getPrice02IncTaxMin != Product.getPrice02IncTaxMax %} ~ {{ Product.getPrice02IncTaxMax|price }}{% endif %}">{{ Product.getPrice02IncTaxMin|price }}{% if Product.getPrice02IncTaxMin != Product.getPrice02IncTaxMax %} ~ {{ Product.getPrice02IncTaxMax|price }}{% endif %}</span>
  677.                     {% else %}
  678.                         <span class="product-price-value price02-default text-color-primary" data-original-price="{{ Product.getPrice02IncTaxMin|price }}">{{ Product.getPrice02IncTaxMin|price }}</span>
  679.                     {% endif %}
  680.                     <span class="price-tax">(税込)</span>
  681.                 </div>
  682.                 <!-- Quantity and Add to Cart -->
  683.                 <form method="post" id="form1" action="{{ url('product_add_cart', {'id': Product.id}) }}">
  684.                 <div class="product-quantity-wrapper">
  685.                     {% if Product.stock_find %}
  686.                         <div class="product-quantity-left{% if not Product.hasProductClass %} d-none{% endif %}">
  687.                             {# <div class="btn-group__header group__header-mobile mb-12">数量</div> #}
  688.                                     {{ form_widget(form._token) }}
  689.                                     {{ form_widget(form.product_id) }}
  690.                                     {{ form_widget(form.ProductClass) }}
  691.                                     {% if form.classcategory_id1 is defined or form.classcategory_id2 is defined %}
  692.                                         <div class="product-classcategory-list">
  693.                                             {% if form.classcategory_id1 is defined %}
  694.                                                 <div class="classcategory-wrapper" data-category-type="1">
  695.                                                     {{ form_widget(form.classcategory_id1, {'attr': {'class': 'classcategory-select'}}) }}
  696.                                                     {{ form_errors(form.classcategory_id1) }}
  697.                                                 </div>
  698.                                             {% endif %}
  699.                                             {% if form.classcategory_id2 is defined %}
  700.                                                 <div class="classcategory-wrapper" data-category-type="2">
  701.                                                     {{ form_widget(form.classcategory_id2, {'attr': {'class': 'classcategory-select'}}) }}
  702.                                                     {{ form_errors(form.classcategory_id2) }}
  703.                                                 </div>
  704.                                             {% endif %}
  705.                                         </div>
  706.                                     {% endif %}
  707.                                     <div class="product-quantity" style="display: none;">
  708.                                         <button type="button" class="quantity-btn quantity-btn--minus item-minus btn-disabled">
  709.                                             <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect y="5" width="12" height="2" fill="#333333"/></svg>
  710.                                         </button>
  711.                                         <div class="quantity-value">
  712.                                             {{ form_widget(form.quantity, {
  713.                                                 'attr': {
  714.                                                     'class': 'custom-quantity hide-number-input h-100 bg-white',
  715.                                                     'readonly': 'readonly',
  716.                                                 }
  717.                                             }) }}
  718.                                         </div>
  719.                                         <button type="button" class="quantity-btn quantity-btn--plus item-plus btn-disabled">
  720.                                             <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 5H12V7H7V12H5V7H0V5H5V0H7V5Z" fill="#333333"/></svg>
  721.                                         </button>
  722.                                         {{ form_errors(form.quantity) }}
  723.                                     </div>
  724.                         </div>
  725.                         <!-- Add to Cart / Favorite -->
  726.                         <div class="product-cart-actions">
  727.                             <button type="submit" class="btn-add-to-cart add-cart">
  728.                                 <span>カートに入れる</span>
  729.                                 <div class="btn-add-to-cart__icon">
  730.                                     <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.252 6.00098L10.249 6.00391L10.252 6.00684L5.72656 10.5312L4.5957 9.40039L7.19434 6.80273H1.72852V5.20312H7.19238L4.5957 2.60742L5.72656 1.47656L10.252 6.00098Z" fill="#FBE800"/></svg>
  731.                                 </div>
  732.                             </button>
  733.                             {% include 'Product/favorite_button.twig' %}
  734.                         </div>
  735.                         {% else %}
  736.                         <!-- button out of stock -->
  737.                         <div class="product-cart-actions">
  738.                             <button class="btn-add-to-cart btn-add-to-cart--out-of-stock" disabled="disabled">
  739.                                 <span>{{ 'front.product.out_of_stock'|trans }}</span>
  740.                             </button>
  741.                             {% include 'Product/favorite_button.twig' %}
  742.                         </div>
  743.                     {% endif %}
  744.                 </div>
  745.                 {% if form.user_imei is defined %}
  746.                     <div class="user-imei-form mt-4">
  747.                         <div class="btn-group__header group__header-mobile mb-12" style="margin-bottom: 16px !important;">IMEI番号</div>
  748.                         {{ form_widget(form.user_imei, {required: false}) }}
  749.                         {{ form_errors(form.user_imei) }}
  750.                     </div>
  751.                 {% endif %}
  752.                 </form>
  753.                 {% if BaseInfo.option_favorite_product %}
  754.                     <form id="favorite-form" action="{{ url('product_add_favorite', {id: Product.id}) }}" method="post" hidden></form>
  755.                 {% endif %}
  756.                 <!-- Divider -->
  757.                 <div class="product-divider"></div>
  758.                 <!-- Product Description Section -->
  759.                 <div class="btn-group__header mb-16">商品説明</div>
  760.                 <div class="product-description">
  761.                     {{ Product.description_detail | nl2br }}
  762.                 </div>
  763.                 {% if Product.product_note is not empty %}
  764.                     <div class="btn-group__header mb-16">備考</div>
  765.                     <div class="product-remarks">{{ Product.product_note | nl2br }}</div>
  766.                 {% endif %}
  767.             </div>
  768.         </div>
  769.         <!-- border bottom -->
  770.         <div class="product-detail-border-bottom"></div>
  771.         <!-- btn bottom -->
  772.         <div class="btn-bottom">
  773.             <button class="btn-return" onclick="history.back()">
  774.                 <div class="btn-return__icon">
  775.                     <svg width="9" height="10" viewBox="0 0 9 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 4.52441L0.00292969 4.52734L0 4.53027L4.52539 9.05469L5.65625 7.92383L3.05762 5.32617H8.52344V3.72656H3.05957L5.65625 1.13086L4.52539 0L0 4.52441Z" fill="#FBE800"/></svg>
  776.                 </div>
  777.                 <span>前のページに戻る</span>
  778.             </button>
  779.         </div>
  780.     </div>
  781.     <!-- Modal -->
  782.     {% include 'CustomComponent/modal.twig' with {
  783.         id: 'cart-success-modal',
  784.         title: 'front.modal.cart_success.title'|trans({}, 'messages'),
  785.         message: 'front.modal.cart_success.message'|trans({}, 'messages'),
  786.         showCancelButton: false,
  787.         showActionButton: true,
  788.         actionText: 'front.modal.cart_success.action_text'|trans({}, 'messages'),
  789.         actionUrl: url('cart'),
  790.         autoRedirect: false,
  791.         redirectUrl: '',
  792.         redirectDelay: 0,
  793.         reloadOnCancel: true,
  794.         reloadOnError: false
  795.     } %}
  796. {% endblock %}