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.     // Tự động chọn option theo product_code (sản phẩm nhiều lựa chọn)
  319.     function autoSelectByProductCode() {
  320.         const productCode = params.get('product_code');
  321.         const $select1 = $('select[name=classcategory_id1]');
  322.         if (!productCode || !eccube.classCategories || !$select1.length) {
  323.             return;
  324.         }
  325.         let matchId1 = null;
  326.         let matchId2 = '';
  327.         Object.keys(eccube.classCategories).some(function(id1) {
  328.             if (id1 === '__unselected') {
  329.                 return false;
  330.             }
  331.             const classcat2Map = eccube.classCategories[id1];
  332.             if (!classcat2Map) {
  333.                 return false;
  334.             }
  335.             return Object.keys(classcat2Map).some(function(key) {
  336.                 const item = classcat2Map[key];
  337.                 if (item && item.product_code && item.product_code === productCode) {
  338.                     matchId1 = id1;
  339.                     matchId2 = item.classcategory_id2 || '';
  340.                     return true;
  341.                 }
  342.                 return false;
  343.             });
  344.         });
  345.         if (!matchId1) {
  346.             return;
  347.         }
  348.         $select1.val(matchId1).trigger('change');
  349.         const $select2 = $('select[name=classcategory_id2]');
  350.         if ($select2.length && matchId2) {
  351.             $select2.val(matchId2).trigger('change');
  352.         }
  353.     }
  354.     // Chạy function khi document ready và sau khi các select đã được khởi tạo
  355.     $(document).ready(function() {
  356.         // Delay một chút để đảm bảo các select đã được khởi tạo
  357.         setTimeout(function() {
  358.             addCountryCategoryAttributes();
  359.             autoSelectByProductCode();
  360.         }, 100);
  361.     });
  362.     // Custom function to update price display
  363.     function updatePriceDisplay(classcat2) {
  364.         var $price02 = $('.price02-default');
  365.         try {
  366.             if (classcat2 && typeof classcat2.price02_inc_tax !== 'undefined' && String(classcat2.price02_inc_tax).length >= 1) {
  367.                 // Show specific price when category is selected
  368.                 $price02.text(classcat2.price02_inc_tax_with_currency).addClass('selected');
  369.             } else {
  370.                 // Show range price when no category is selected
  371.                 var originalPrice = $price02.attr('data-original-price');
  372.                 if (originalPrice) {
  373.                     $price02.text(originalPrice).removeClass('selected');
  374.                 }
  375.             }
  376.         } catch (e) {
  377.             console.log('Error updating price display:', e);
  378.             // Fallback to original price
  379.             var originalPrice = $price02.attr('data-original-price');
  380.             if (originalPrice) {
  381.                 $price02.text(originalPrice).removeClass('selected');
  382.             }
  383.         }
  384.     }
  385.     // Add event listeners for category changes
  386.     $(document).ready(function() {
  387.         // Listen for category 1 changes
  388.         $('select[name=classcategory_id1]').on('change', function() {
  389.             var $form = $(this).parents('form');
  390.             var product_id = $form.find('input[name=product_id]').val();
  391.             var $sele1 = $(this);
  392.             var $sele2 = $form.find('select[name=classcategory_id2]');
  393.             // Update price display after a short delay to let original function complete
  394.             setTimeout(function() {
  395.                 updatePriceFromCategories(product_id, $sele1.val(), $sele2.val());
  396.             }, 150);
  397.         });
  398.         // Listen for category 2 changes
  399.         $('select[name=classcategory_id2]').on('change', function() {
  400.             var $form = $(this).parents('form');
  401.             var product_id = $form.find('input[name=product_id]').val();
  402.             var $sele1 = $form.find('select[name=classcategory_id1]');
  403.             var $sele2 = $form.find('select[name=classcategory_id2]');
  404.             // Update price display after a short delay to let original function complete
  405.             setTimeout(function() {
  406.                 updatePriceFromCategories(product_id, $sele1.val(), $sele2.val());
  407.             }, 150);
  408.         });
  409.     });
  410.     // Function to update price based on selected categories
  411.     function updatePriceFromCategories(product_id, classcat_id1, classcat_id2) {
  412.         try {
  413.             // Don't update if categories are not properly selected
  414.             if (!classcat_id1 || classcat_id1 === '__unselected' || classcat_id1 === '') {
  415.                 var $price02 = $('.price02-default');
  416.                 var originalPrice = $price02.attr('data-original-price');
  417.                 if (originalPrice) {
  418.                     $price02.text(originalPrice).removeClass('selected');
  419.                 }
  420.                 return;
  421.             }
  422.             // Normalize: khi classcat_id2 là null hoặc __unselected phải dùng '' để lookup đúng key '#'
  423.             var classcat2Key = '#' + (classcat_id2 && classcat_id2 !== '__unselected' ? classcat_id2 : '');
  424.             var classcat2;
  425.             if (eccube.hasOwnProperty('productsClassCategories')) {
  426.                 if (eccube.productsClassCategories[product_id] &&
  427.                     eccube.productsClassCategories[product_id][classcat_id1]) {
  428.                     classcat2 = eccube.productsClassCategories[product_id][classcat_id1][classcat2Key];
  429.                 }
  430.             } else {
  431.                 if (typeof eccube.classCategories[classcat_id1] !== 'undefined') {
  432.                     classcat2 = eccube.classCategories[classcat_id1][classcat2Key];
  433.                 }
  434.             }
  435.             updatePriceDisplay(classcat2);
  436.         } catch (e) {
  437.             console.log('Error updating price from categories:', e);
  438.             // Fallback to original price
  439.             var $price02 = $('.price02-default');
  440.             var originalPrice = $price02.attr('data-original-price');
  441.             if (originalPrice) {
  442.                 $price02.text(originalPrice).removeClass('selected');
  443.             }
  444.         }
  445.     }
  446.     // Initialize price display when page loads
  447.     $(document).ready(function() {
  448.         // Store original price if not already stored
  449.         var $price02 = $('.price02-default');
  450.         if (!$price02.attr('data-original-price')) {
  451.             $price02.attr('data-original-price', $price02.text());
  452.         }
  453.         // Check if any category is selected on page load
  454.         var $classcat1 = $('select[name=classcategory_id1]');
  455.         var $classcat2 = $('select[name=classcategory_id2]');
  456.         // Only update price if categories are properly selected
  457.         if ($classcat1.length && $classcat1.val() && $classcat1.val() !== '__unselected') {
  458.             // Category 1 is selected, trigger price update
  459.             if ($classcat2.length && $classcat2.val() && $classcat2.val() !== '__unselected') {
  460.                 // Both categories are selected
  461.                 setTimeout(function() {
  462.                     updatePriceFromCategories({{ Product.id }}, $classcat1.val(), $classcat2.val());
  463.                 }, 200);
  464.             } else {
  465.                 // Only category 1 is selected
  466.                 setTimeout(function() {
  467.                     updatePriceFromCategories({{ Product.id }}, $classcat1.val(), null);
  468.                 }, 200);
  469.             }
  470.         }
  471.     });
  472. </script>
  473.     <script>
  474.         $(function() {
  475.             // bfcache無効化
  476.             $(window).bind('pageshow', function(event) {
  477.                 if (event.originalEvent.persisted) {
  478.                     location.reload(true);
  479.                 }
  480.             });
  481.             // Core Web Vital の Cumulative Layout Shift(CLS)対策のため
  482.             // img タグに width, height が付与されている.
  483.             // 630px 未満の画面サイズでは縦横比が壊れるための対策
  484.             // see https://github.com/EC-CUBE/ec-cube/pull/5023
  485.             $('.ec-grid2__cell').hide();
  486.             var removeSize = function () {
  487.                 $('.slide-item').height('');
  488.                 $('.slide-item img')
  489.                     .removeAttr('width')
  490.                     .removeAttr('height')
  491.                     .removeAttr('style');
  492.             };
  493.             var slickInitial = function(slick) {
  494.                 $('.ec-grid2__cell').fadeIn(1500);
  495.                 var baseHeight = $(slick.target).height();
  496.                 var baseWidth = $(slick.target).width();
  497.                 var rate = baseWidth / baseHeight;
  498.                 $('.slide-item').height(baseHeight * rate); // 余白を削除する
  499.                 // transform を使用することでCLSの影響を受けないようにする
  500.                 $('.slide-item img')
  501.                     .css(
  502.                         {
  503.                             'transform-origin': 'top left',
  504.                             'transform': 'scaleY(' + rate + ')',
  505.                             'transition': 'transform .1s'
  506.                         }
  507.                     );
  508.                 // 正しいサイズに近くなったら属性を解除する
  509.                 setTimeout(removeSize, 500);
  510.             };
  511.             $('.item_visual').on('init', slickInitial);
  512.             // リサイズ時は CLS の影響を受けないため属性を解除する
  513.             $(window).resize(removeSize);
  514.             $('.item_visual').slick({
  515.                 dots: false,
  516.                 arrows: false
  517.             });
  518.             $('.slideThumb').on('click', function() {
  519.                 var index = $(this).attr('data-index');
  520.                 $('.item_visual').slick('slickGoTo', index, false);
  521.             })
  522.         });
  523.     </script>
  524.     <script>
  525.         $(function() {
  526.             {% set just_added_favorite = app.session.flashbag.get('product_detail.just_added_favorite') %}
  527.             {% if just_added_favorite and Product.id == just_added_favorite[0] %}
  528.                 alert('{{ 'front.product.added_to_favorite'|trans }}');
  529.             {% endif %}
  530.             $('.add-cart').on('click', function(event) {
  531.                 {% if form.classcategory_id1 is defined %}
  532.                     // 規格1フォームの必須チェック
  533.                     if ($('#classcategory_id1').val() == '__unselected' || $('#classcategory_id1').val() == '') {
  534.                         $('#classcategory_id1')[0].setCustomValidity('{{ 'front.product.product_class_unselected'|trans }}');
  535.                         return true;
  536.                     } else {
  537.                         $('#classcategory_id1')[0].setCustomValidity('');
  538.                     }
  539.                 {% endif %}
  540.                 {% if form.classcategory_id2 is defined %}
  541.                     // 規格2フォームの必須チェック
  542.                     if ($('#classcategory_id2').val() == '__unselected' || $('#classcategory_id2').val() == '') {
  543.                         $('#classcategory_id2')[0].setCustomValidity('{{ 'front.product.product_class_unselected'|trans }}');
  544.                         return true;
  545.                     } else {
  546.                         $('#classcategory_id2')[0].setCustomValidity('');
  547.                     }
  548.                 {% endif %}
  549.                 // 個数フォームのチェック
  550.                 if ($('#quantity').val() < 1) {
  551.                     $('#quantity')[0].setCustomValidity('{{ 'front.product.invalid_quantity'|trans }}');
  552.                     return true;
  553.                 } else {
  554.                     $('#quantity')[0].setCustomValidity('');
  555.                 }
  556.                 // IMEIフォームのチェック
  557.                 {% if Product.hasUserImeiCategory %}
  558.                     var $imei = $('#user_imei');
  559.                     var imeiRequiredMsg = '{{ 'front.cart.user_imei_required'|trans }}';
  560.                     if (!$imei.length || !$.trim($imei.val() || '')) {
  561.                         event.preventDefault();
  562.                         if ($imei.length && window.CustomerInputValidate) {
  563.                             CustomerInputValidate.showInlineError($imei, imeiRequiredMsg, '.user-imei-form');
  564.                             CustomerInputValidate.scrollAndFlash($imei, { wrapperSelector: '.user-imei-form' });
  565.                         } else if ($imei.length) {
  566.                             alert(imeiRequiredMsg);
  567.                         } else {
  568.                             alert(imeiRequiredMsg);
  569.                         }
  570.                         return false;
  571.                     } else if ($imei.length && window.CustomerInputValidate) {
  572.                         CustomerInputValidate.clearInlineError($imei, '.user-imei-form');
  573.                     }
  574.                 {% endif %}
  575.                 event.preventDefault();
  576.                 $form = $('#form1');
  577.                 $.ajax({
  578.                     url: $form.attr('action'),
  579.                     type: $form.attr('method'),
  580.                     data: $form.serialize(),
  581.                     dataType: 'json',
  582.                     beforeSend: function(xhr, settings) {
  583.                         // Buttonを無効にする
  584.                         $('.add-cart').prop('disabled', true);
  585.                     }
  586.                 }).done(function(data) {
  587.                     if (data.error_code == 'OUT_OF_STOCK_ZERO' || data.error_code == 'OUT_OF_STOCK') {
  588.                         alert('この商品の在庫は残り' + data.stock_left + 'です。');
  589.                     } else if (data.error_code == 'DUPLICATE_IMEI') {
  590.                         {% if form.user_imei is defined %}
  591.                         if (window.CustomerInputValidate && $('#user_imei').length) {
  592.                             CustomerInputValidate.showInlineError($('#user_imei'), '{{ 'front.shopping.duplicate_imei'|trans }}', '.user-imei-form');
  593.                             CustomerInputValidate.scrollAndFlash($('#user_imei'), { wrapperSelector: '.user-imei-form' });
  594.                         } else {
  595.                             alert('{{ 'front.shopping.duplicate_imei'|trans }}');
  596.                         }
  597.                         {% else %}
  598.                         alert('{{ 'front.shopping.duplicate_imei'|trans }}');
  599.                         {% endif %}
  600.                     } else if (data.error_code == 'CART_HAS_PRODUCT') {
  601.                         alert('{{ 'front.shopping.cart_has_product'|trans }}');
  602.                     } else if (data.error_code == 'IMEI_VALIDATION_FAILED') {
  603.                         {% if form.user_imei is defined %}
  604.                         if (window.CustomerInputValidate && $('#user_imei').length) {
  605.                             CustomerInputValidate.showInlineError($('#user_imei'), '{{ 'front.shopping.imei_validation_failed'|trans }}', '.user-imei-form');
  606.                             CustomerInputValidate.scrollAndFlash($('#user_imei'), { wrapperSelector: '.user-imei-form' });
  607.                         } else {
  608.                             alert('{{ 'front.shopping.imei_validation_failed'|trans }}');
  609.                         }
  610.                         {% else %}
  611.                         alert('{{ 'front.shopping.imei_validation_failed'|trans }}');
  612.                         {% endif %}
  613.                     } else {
  614.                         // カートブロックを更新する
  615.                         $.ajax({
  616.                             url: "{{ url('block_cart') }}",
  617.                             type: 'GET',
  618.                             dataType: 'html'
  619.                         }).done(function(html) {
  620.                             $('.ec-headerRole__cart').html(html);
  621.                         });
  622.                         // Hiển thị popup thông báo thành công sử dụng ModalComponent
  623.                         if (window.ModalUtils) {
  624.                             window.ModalUtils.showSuccess('cart-success-modal',
  625.                                 '{{ 'front.modal.cart_success.title'|trans({}, 'messages') }}',
  626.                                 '{{ 'front.modal.cart_success.message'|trans({}, 'messages') }}'
  627.                             );
  628.                         } else {
  629.                             // Fallback nếu ModalComponent chưa load
  630.                             alert('{{ 'front.modal.cart_success.title'|trans({}, 'messages') }}');
  631.                         }
  632.                     }
  633.                 }).fail(function(data) {
  634.                     alert('{{ 'front.product.add_cart_error'|trans }}');
  635.                 }).always(function(data) {
  636.                     // Buttonを有効にする
  637.                     $('.add-cart').prop('disabled', false);
  638.                 });
  639.             });
  640.         });
  641.         // Modal events are now handled by ModalComponent
  642.     </script>
  643.     <script src="{{ asset('assets/CustomComponent/modal.js') }}"></script>
  644.     </script>
  645.     <script type="application/ld+json">
  646.     {
  647.         "@context": "https://schema.org/",
  648.         "@type": "Product",
  649.         "name": "{{ Product.name }}",
  650.         "image": [
  651.             {% for img in Product.ProductImage %}
  652.                 "{{ app.request.schemeAndHttpHost }}{{ asset(img, 'save_image') }}"{% if not loop.last %},{% endif %}
  653.             {% else %}
  654.                 "{{ app.request.schemeAndHttpHost }}{{ asset(''|no_image_product, 'save_image') }}"
  655.             {% endfor %}
  656.         ],
  657.         "description": "{{ Product.description_list | default(Product.description_detail) | replace({'\n': '', '\r': ''}) | slice(0,300) }}",
  658.         {% if Product.code_min %}
  659.         "sku": "{{ Product.code_min }}",
  660.         {% endif %}
  661.         "offers": {
  662.             "@type": "Offer",
  663.             "url": "{{ url('product_detail', {'id': Product.id}) }}",
  664.             "priceCurrency": "{{ eccube_config.currency }}",
  665.             "price": {{ Product.getPrice02IncTaxMin ? Product.getPrice02IncTaxMin : 0}},
  666.             "availability": "{{ Product.stock_find ? "InStock" : "OutOfStock" }}"
  667.         }
  668.     }
  669.     </script>
  670. {% endblock %}
  671. {% block main %}
  672.     <!-- breadcrumb section -->
  673.     <div class="breadcrumb-section">
  674.         <div class="container-1360 border-box px-40">
  675.             <nav class="breadcrumb">
  676.                 <a href="{{ url('homepage') }}" class="breadcrumb__item text-black">ホーム</a>
  677.                 <img src="{{ asset('assets/img/default/icons/icon-breadcrumb-arrow.svg') }}" alt=">" class="breadcrumb__separator">
  678.                 <a href="{{ url('product_list') }}" class="breadcrumb__item text-black">商品一覧</a>
  679.                 <img src="{{ asset('assets/img/default/icons/icon-breadcrumb-arrow.svg') }}" alt=">" class="breadcrumb__separator">
  680.                 <span class="breadcrumb__item breadcrumb__item--current text-black">{{ Product.name }}</span>
  681.             </nav>
  682.         </div>
  683.     </div>
  684.     <!-- product detail section -->
  685.     <div class="product-detail-section container-1200">
  686.         <div class="product-detail-wrapper">
  687.             <div class="product-detail-left">
  688.                 <!-- Main gallery (using existing slick) -->
  689.                 <div class="item_visual">
  690.                     {% for ProductImage in Product.ProductImage %}
  691.                         <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>
  692.                     {% else %}
  693.                         <div class="slide-item"><img src="{{ asset(''|no_image_product, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" width="550" height="550"></div>
  694.                     {% endfor %}
  695.                 </div>
  696.                 <!-- Thumbs below -->
  697.                 <div class="product-detail-left-bottom">
  698.                     <div class="item_nav">
  699.                         {% for ProductImage in Product.ProductImage %}
  700.                             <div class="slideThumb" data-index="{{ loop.index0 }}">
  701.                                 <div class="product-thumbnail"><img src="{{ asset(ProductImage, 'save_image') }}" alt="" width="133" height="133" loading="lazy"></div>
  702.                             </div>
  703.                         {% endfor %}
  704.                     </div>
  705.                 </div>
  706.             </div>
  707.             <div class="product-detail-right">
  708.                 <!-- Product Title -->
  709.                 <h1 class="product-title">{{ Product.name }}</h1>
  710.                 <!-- Product Price -->
  711.                 <div class="product-price">
  712.                     {% if Product.hasProductClass -%}
  713.                         <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>
  714.                     {% else %}
  715.                         <span class="product-price-value price02-default text-color-primary" data-original-price="{{ Product.getPrice02IncTaxMin|price }}">{{ Product.getPrice02IncTaxMin|price }}</span>
  716.                     {% endif %}
  717.                     <span class="price-tax">(税込)</span>
  718.                 </div>
  719.                 <!-- Quantity and Add to Cart -->
  720.                 <form method="post" id="form1" action="{{ url('product_add_cart', {'id': Product.id}) }}">
  721.                 <div class="product-quantity-wrapper">
  722.                     {% if Product.stock_find %}
  723.                         <div class="product-quantity-left{% if not Product.hasProductClass %} d-none{% endif %}">
  724.                             {# <div class="btn-group__header group__header-mobile mb-12">数量</div> #}
  725.                                     {{ form_widget(form._token) }}
  726.                                     {{ form_widget(form.product_id) }}
  727.                                     {{ form_widget(form.ProductClass) }}
  728.                                     {% if form.classcategory_id1 is defined or form.classcategory_id2 is defined %}
  729.                                         <div class="product-classcategory-list">
  730.                                             {% if form.classcategory_id1 is defined %}
  731.                                                 <div class="classcategory-wrapper" data-category-type="1">
  732.                                                     {{ form_widget(form.classcategory_id1, {'attr': {'class': 'classcategory-select'}}) }}
  733.                                                     {{ form_errors(form.classcategory_id1) }}
  734.                                                 </div>
  735.                                             {% endif %}
  736.                                             {% if form.classcategory_id2 is defined %}
  737.                                                 <div class="classcategory-wrapper" data-category-type="2">
  738.                                                     {{ form_widget(form.classcategory_id2, {'attr': {'class': 'classcategory-select'}}) }}
  739.                                                     {{ form_errors(form.classcategory_id2) }}
  740.                                                 </div>
  741.                                             {% endif %}
  742.                                         </div>
  743.                                     {% endif %}
  744.                                     <div class="product-quantity" style="display: none;">
  745.                                         <button type="button" class="quantity-btn quantity-btn--minus item-minus btn-disabled">
  746.                                             <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect y="5" width="12" height="2" fill="#333333"/></svg>
  747.                                         </button>
  748.                                         <div class="quantity-value">
  749.                                             {{ form_widget(form.quantity, {
  750.                                                 'attr': {
  751.                                                     'class': 'custom-quantity hide-number-input h-100 bg-white',
  752.                                                     'readonly': 'readonly',
  753.                                                 }
  754.                                             }) }}
  755.                                         </div>
  756.                                         <button type="button" class="quantity-btn quantity-btn--plus item-plus btn-disabled">
  757.                                             <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>
  758.                                         </button>
  759.                                         {{ form_errors(form.quantity) }}
  760.                                     </div>
  761.                         </div>
  762.                         <!-- Add to Cart / Favorite -->
  763.                         <div class="product-cart-actions">
  764.                             <button type="submit" class="btn-add-to-cart add-cart">
  765.                                 <span>カートに入れる</span>
  766.                                 <div class="btn-add-to-cart__icon">
  767.                                     <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>
  768.                                 </div>
  769.                             </button>
  770.                             {% include 'Product/favorite_button.twig' %}
  771.                         </div>
  772.                         {% else %}
  773.                         <!-- button out of stock -->
  774.                         <div class="product-cart-actions">
  775.                             <button class="btn-add-to-cart btn-add-to-cart--out-of-stock" disabled="disabled">
  776.                                 <span>{{ 'front.product.out_of_stock'|trans }}</span>
  777.                             </button>
  778.                             {% include 'Product/favorite_button.twig' %}
  779.                         </div>
  780.                     {% endif %}
  781.                 </div>
  782.                 {% if form.user_imei is defined %}
  783.                     <div class="user-imei-form mt-4">
  784.                         <div class="btn-group__header group__header-mobile mb-12" style="margin-bottom: 16px !important;">IMEI番号</div>
  785.                         {{ form_widget(form.user_imei, {required: false}) }}
  786.                         {{ form_errors(form.user_imei) }}
  787.                     </div>
  788.                 {% endif %}
  789.                 </form>
  790.                 {% if BaseInfo.option_favorite_product %}
  791.                     <form id="favorite-form" action="{{ url('product_add_favorite', {id: Product.id}) }}" method="post" hidden></form>
  792.                 {% endif %}
  793.                 <!-- Divider -->
  794.                 <div class="product-divider"></div>
  795.                 <!-- Product Description Section -->
  796.                 <div class="btn-group__header mb-16">商品説明</div>
  797.                 <div class="product-description">
  798.                     {{ Product.description_detail | nl2br }}
  799.                 </div>
  800.                 {% if Product.product_note is not empty %}
  801.                     <div class="btn-group__header mb-16">備考</div>
  802.                     <div class="product-remarks">{{ Product.product_note | nl2br }}</div>
  803.                 {% endif %}
  804.             </div>
  805.         </div>
  806.         <!-- border bottom -->
  807.         <div class="product-detail-border-bottom"></div>
  808.         <!-- btn bottom -->
  809.         <div class="btn-bottom">
  810.             <button class="btn-return" onclick="history.back()">
  811.                 <div class="btn-return__icon">
  812.                     <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>
  813.                 </div>
  814.                 <span>前のページに戻る</span>
  815.             </button>
  816.         </div>
  817.     </div>
  818.     <!-- Modal -->
  819.     {% include 'CustomComponent/modal.twig' with {
  820.         id: 'cart-success-modal',
  821.         title: 'front.modal.cart_success.title'|trans({}, 'messages'),
  822.         message: 'front.modal.cart_success.message'|trans({}, 'messages'),
  823.         showCancelButton: false,
  824.         showActionButton: true,
  825.         actionText: 'front.modal.cart_success.action_text'|trans({}, 'messages'),
  826.         actionUrl: url('cart'),
  827.         autoRedirect: false,
  828.         redirectUrl: '',
  829.         redirectDelay: 0,
  830.         reloadOnCancel: true,
  831.         reloadOnError: false
  832.     } %}
  833. {% endblock %}