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

Open in your IDE?
  1. {% extends 'default_frame.twig' %}
  2. {% set body_class = 'product_page' %}
  3. {% import _self as stars %}
  4. {# 星テキスト生成用マクロ #}
  5. {% macro stars(positive, negative) %}
  6.     {% set positive_stars = ["", "★", "★★", "★★★", "★★★★", "★★★★★"] %}
  7.     {% set negative_stars = ["", "☆", "☆☆", "☆☆☆", "☆☆☆☆", "☆☆☆☆☆"] %}
  8.     {{ positive_stars[positive] }}{{ negative_stars[negative] }}
  9. {% endmacro %}
  10. {% block stylesheet %}
  11.     <link rel="stylesheet" href="{{ asset('assets/css/product-details.css', 'user_data') }}" />
  12.     <style>
  13.         .slick-slider {
  14.             margin-bottom: 30px;
  15.         }
  16.         .slick-dots {
  17.             position: absolute;
  18.             bottom: -45px;
  19.             display: block;
  20.             width: 100%;
  21.             padding: 0;
  22.             list-style: none;
  23.             text-align: center;
  24.         }
  25.         .slick-dots li {
  26.             position: relative;
  27.             display: inline-block;
  28.             width: 20px;
  29.             height: 20px;
  30.             margin: 0 5px;
  31.             padding: 0;
  32.             cursor: pointer;
  33.         }
  34.         .slick-dots li button {
  35.             font-size: 0;
  36.             line-height: 0;
  37.             display: block;
  38.             width: 20px;
  39.             height: 20px;
  40.             padding: 5px;
  41.             cursor: pointer;
  42.             color: transparent;
  43.             border: 0;
  44.             outline: none;
  45.             background: transparent;
  46.         }
  47.         .slick-dots li button:hover,
  48.         .slick-dots li button:focus {
  49.             outline: none;
  50.         }
  51.         .slick-dots li button:hover:before,
  52.         .slick-dots li button:focus:before {
  53.             opacity: 1;
  54.         }
  55.         .slick-dots li button:before {
  56.             content: " ";
  57.             line-height: 20px;
  58.             position: absolute;
  59.             top: 0;
  60.             left: 0;
  61.             width: 12px;
  62.             height: 12px;
  63.             text-align: center;
  64.             opacity: .25;
  65.             background-color: black;
  66.             border-radius: 50%;
  67.         }
  68.         .slick-dots li.slick-active button:before {
  69.             opacity: .75;
  70.             background-color: black;
  71.         }
  72.         .slick-dots li button.thumbnail img {
  73.             width: 0;
  74.             height: 0;
  75.         }
  76.     </style>
  77. {% endblock %}
  78. {% block javascript %}
  79.     <script>
  80.         eccube.classCategories = {{ class_categories_as_json(Product)|raw }};
  81.         // 規格2に選択肢を割り当てる。
  82.         function fnSetClassCategories(form, classcat_id2_selected) {
  83.             var $form = $(form);
  84.             var product_id = $form.find('input[name=product_id]').val();
  85.             var $sele1 = $form.find('select[name=classcategory_id1]');
  86.             var $sele2 = $form.find('select[name=classcategory_id2]');
  87.             eccube.setClassCategories($form, product_id, $sele1, $sele2, classcat_id2_selected);
  88.         }
  89.         {% if form.classcategory_id2 is defined %}
  90.         fnSetClassCategories(
  91.             $('#form1'), {{ form.classcategory_id2.vars.value|json_encode|raw }}
  92.         );
  93.         {% elseif form.classcategory_id1 is defined %}
  94.         eccube.checkStock($('#form1'), {{ Product.id }}, {{ form.classcategory_id1.vars.value|json_encode|raw }}, null);
  95.         {% endif %}
  96.     </script>
  97.     <script>
  98.         $(function() {
  99.             // bfcache無効化
  100.             $(window).bind('pageshow', function(event) {
  101.                 if (event.originalEvent.persisted) {
  102.                     location.reload(true);
  103.                 }
  104.             });
  105.             // Core Web Vital の Cumulative Layout Shift(CLS)対策のため
  106.             // img タグに width, height が付与されている.
  107.             // 630px 未満の画面サイズでは縦横比が壊れるための対策
  108.             // see https://github.com/EC-CUBE/ec-cube/pull/5023
  109.             $('.ec-grid2__cell').hide();
  110.             var removeSize = function () {
  111.                 $('.slide-item').height('');
  112.                 $('.slide-item img')
  113.                     .removeAttr('width')
  114.                     .removeAttr('height')
  115.                     .removeAttr('style');
  116.             };
  117.             var slickInitial = function(slick) {
  118.                 $('.ec-grid2__cell').fadeIn(1500);
  119.                 var baseHeight = $(slick.target).height();
  120.                 var baseWidth = $(slick.target).width();
  121.                 var rate = baseWidth / baseHeight;
  122.                 $('.slide-item').height(baseHeight * rate); // 余白を削除する
  123.                 // transform を使用することでCLSの影響を受けないようにする
  124.                 $('.slide-item img')
  125.                     .css(
  126.                         {
  127.                             'transform-origin': 'top left',
  128.                             'transform': 'scaleY(' + rate + ')',
  129.                             'transition': 'transform .1s'
  130.                         }
  131.                     );
  132.                 // 正しいサイズに近くなったら属性を解除する
  133.                 setTimeout(removeSize, 500);
  134.             };
  135.             $('.item_visual').on('init', slickInitial);
  136.             // リサイズ時は CLS の影響を受けないため属性を解除する
  137.             $(window).resize(removeSize);
  138.             $('.item_visual').slick({
  139.                 dots: false,
  140.                 arrows: false,
  141.                 responsive: [{
  142.                     breakpoint: 768,
  143.                     settings: {
  144.                         dots: true
  145.                     }
  146.                 }]
  147.             });
  148.             $('.slideThumb').on('click', function() {
  149.                 var index = $(this).attr('data-index');
  150.                 $('.item_visual').slick('slickGoTo', index, false);
  151.             })
  152.         });
  153.     </script>
  154.     <script>
  155.         $(function() {
  156.             $('.add-cart').on('click', function(event) {
  157.                 {% if form.classcategory_id1 is defined %}
  158.                 // 規格1フォームの必須チェック
  159.                 if ($('#classcategory_id1').val() == '__unselected' || $('#classcategory_id1').val() == '') {
  160.                     $('#classcategory_id1')[0].setCustomValidity('{{ '項目が選択されていません'|trans }}');
  161.                     return true;
  162.                 } else {
  163.                     $('#classcategory_id1')[0].setCustomValidity('');
  164.                 }
  165.                 {% endif %}
  166.                 {% if form.classcategory_id2 is defined %}
  167.                 // 規格2フォームの必須チェック
  168.                 if ($('#classcategory_id2').val() == '__unselected' || $('#classcategory_id2').val() == '') {
  169.                     $('#classcategory_id2')[0].setCustomValidity('{{ '項目が選択されていません'|trans }}');
  170.                     return true;
  171.                 } else {
  172.                     $('#classcategory_id2')[0].setCustomValidity('');
  173.                 }
  174.                 {% endif %}
  175.                 // 個数フォームのチェック
  176.                 if ($('#quantity').val() < 1) {
  177.                     $('#quantity')[0].setCustomValidity('{{ '1以上で入力してください。'|trans }}');
  178.                     return true;
  179.                 } else {
  180.                     $('#quantity')[0].setCustomValidity('');
  181.                 }
  182.                 event.preventDefault();
  183.                 $form = $('#form1');
  184.                 $.ajax({
  185.                     url: $form.attr('action'),
  186.                     type: $form.attr('method'),
  187.                     data: $form.serialize(),
  188.                     dataType: 'json',
  189.                     beforeSend: function(xhr, settings) {
  190.                         // Buttonを無効にする
  191.                         $('.add-cart').prop('disabled', true);
  192.                     }
  193.                 }).done(function(data) {
  194.                     // レスポンス内のメッセージをalertで表示
  195.                     $.each(data.messages, function() {
  196.                         $('#ec-modal-header').text(this);
  197.                     });
  198.                     $('.ec-modal').show()
  199.                     // カートブロックを更新する
  200.                     $.ajax({
  201.                         url: "{{ url('block_cart') }}",
  202.                         type: 'GET',
  203.                         dataType: 'html'
  204.                     }).done(function(html) {
  205.                         $('.ec-headerRole__cart').html(html);
  206.                     });
  207.                 }).fail(function(data) {
  208.                     alert('{{ 'カートへの追加に失敗しました。'|trans }}');
  209.                 }).always(function(data) {
  210.                     // Buttonを有効にする
  211.                     $('.add-cart').prop('disabled', false);
  212.                 });
  213.             });
  214.         });
  215.         $('.ec-modal-wrap').on('click', function(e) {
  216.             // モーダル内の処理は外側にバブリングさせない
  217.             e.stopPropagation();
  218.         });
  219.         $('.ec-modal-overlay, .ec-modal, .ec-modal-close, .ec-inlineBtn--cancel').on('click', function() {
  220.             $('.ec-modal').hide()
  221.         });
  222.     </script>
  223.     <script type="application/ld+json">
  224.     {
  225.         "@context": "https://schema.org/",
  226.         "@type": "Product",
  227.         "name": "{{ Product.name }}",
  228.         "image": [
  229.             {% for img in Product.ProductImage %}
  230.                 "{{ app.request.schemeAndHttpHost }}{{ asset(img, 'save_image') }}"{% if not loop.last %},{% endif %}
  231.             {% else %}
  232.                 "{{ app.request.schemeAndHttpHost }}{{ asset(''|no_image_product, 'save_image') }}"
  233.             {% endfor %}
  234.         ],
  235.         "description": "{{ Product.description_list | default(Product.description_detail) | replace({'\n': '', '\r': ''}) | slice(0,300) }}",
  236.         {% if Product.code_min %}
  237.         "sku": "{{ Product.code_min }}",
  238.         {% endif %}
  239.         "offers": {
  240.             "@type": "Offer",
  241.             "url": "{{ url('product_detail', {'id': Product.id}) }}",
  242.             "priceCurrency": "{{ eccube_config.currency }}",
  243.             "price": {{ Product.getPrice02IncTaxMin ? Product.getPrice02IncTaxMin : 0}},
  244.             "availability": "{{ Product.stock_find ? "InStock" : "OutOfStock" }}"
  245.         }
  246.     }
  247.     </script>
  248. {% endblock %}
  249. {% block main %}
  250.     <div class="common-bg">
  251.         {{ include('Block/pankuzu.twig') }}
  252.         <div class="content__wrapper">
  253.             <section class="product-detail__content">
  254.                 <div class="slide__change-sp">
  255.                     <div class="slide__change__img active">
  256.                         {% for ProductImage in Product.ProductImage %}
  257.                             <img src="{{ asset(ProductImage, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" {% if loop.index > 1 %} loading="lazy"{% endif %}>
  258.                         {% else %}
  259.                             <img src="{{ asset(''|no_image_product, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}">
  260.                         {% endfor %}
  261.                     </div>
  262.                     <div class="slide__change__img__list__wrap slide__change__img__list__wrap-sp">
  263.                         <ul class="slide__change__img__list" id="slide__change__img__list01">
  264.                             {% for ProductImage in Product.ProductImage %}
  265.                                 <li class="slide__change__img__list-item">
  266.                                     <img src="{{ asset(ProductImage, 'save_image') }}" alt=""
  267.                                     class="slide__change__img__list-item__img" loading="lazy">
  268.                                 </li>
  269.                             {% endfor %}
  270.                         </ul>
  271.                     </div>
  272.                     <div class="explanation explanation-pc">
  273.                         <ul class="explanation__info">
  274.                             <li>
  275.                                 <span class="explanation__info__txt">
  276.                                     {{ Product.description_detail|raw|nl2br }}
  277.                                 </span>
  278.                             </li>
  279.                         </ul>
  280.                     </div>
  281.                 </div>
  282.                 <div class="product-details__info__wrap">
  283.                     <h1 class="product-details__info__name product-details__line">{{ Product.name }}</h1>
  284.                     <div class="product-details__info__price product-details__line">
  285.                         {# 通常価格 #}
  286.                         <div class="product-details__info__price01">
  287.                             {{ '通常価格'|trans }}:
  288.                             {% if Product.hasProductClass -%}
  289.                                 {% if Product.getPrice01Min is not null and Product.getPrice01IncTaxMin == Product.getPrice01IncTaxMax %}
  290.                                     {{ Product.getPrice01IncTaxMin|price }}
  291.                                 {% elseif Product.getPrice01Min is not null and Product.getPrice01Max is not null %}
  292.                                     {{ Product.getPrice01IncTaxMin|price }}~ {{ Product.getPrice01IncTaxMax|price }}
  293.                                 {% endif %}
  294.                             {% else %}
  295.                                 {% if Product.getPrice01Max is not null %}
  296.                                     {{ Product.getPrice01IncTaxMin|price }}
  297.                                 {% endif %}
  298.                             {% endif %}
  299.                         ({{ '税込'|trans }})</div>
  300.                         {# 販売価格 #}
  301.                         <div class="product-details__info__price02">
  302.                             {% if Product.hasProductClass -%}
  303.                                 {% if Product.getPrice02IncTaxMin == Product.getPrice02IncTaxMax %}
  304.                                     {{ Product.getPrice02IncTaxMin|price }}
  305.                                 {% else %}
  306.                                     {{ Product.getPrice02IncTaxMin|price }} ~ {{ Product.getPrice02IncTaxMax|price }}
  307.                                 {% endif %}
  308.                             {% else %}
  309.                                 {{ Product.getPrice02IncTaxMin|price }}
  310.                             {% endif %}
  311.                             <span>({{ '税込'|trans }})</span>    
  312.                         </div>
  313.                     </div>
  314.                     {% if Product.code_min is not empty %}
  315.                         <div class="product-details__info__content03 product-details__line">{{ '商品コード'|trans }}:{{ Product.code_min }}{% if Product.code_min != Product.code_max %} ~ {{ Product.code_max }}{% endif %}</div>
  316.                     {% endif %}
  317.                     <div class="product-details__info__content04 product-details__line">
  318.                         ※こちらの価格には消費税が含まれています。<br>※送料は別途発生いたします。詳細はこちら
  319.                     </div>
  320.                     {% if Product.ProductCategories is not empty %}
  321.                         <div class="product-details__info__content05 product-details__line">
  322.                             {{ '関連カテゴリ'|trans }}<br>
  323.                             {% for ProductCategory in Product.ProductCategories %}
  324.                                 {% for Category in ProductCategory.Category.path %}
  325.                                     <a href="{{ url('product_list') }}?category_id={{ Category.id }}">{{ Category.name }}</a> {%- if loop.last == false %}
  326.                                     <span>></span>{% endif -%}
  327.                                 {% endfor %}
  328.                             {% endfor %}
  329.                         </div>
  330.                     {% endif %}
  331.                     <form action="{{ url('product_add_cart', {id:Product.id}) }}" method="post" id="form1" name="form1">
  332.                         {% if Product.stock_find %}
  333.                             {% if form.classcategory_id1 is defined %}
  334.                                 <div class="product-details__info__color product-details__info__item">
  335.                                     <div class="product-details__info__dt">規格</div>
  336.                                     <div class="form__input quantity">
  337.                                         {{ form_row(form.classcategory_id1) }}
  338.                                         {{ form_errors(form.classcategory_id1) }}
  339.                                     </div>
  340.                                 </div>
  341.                                 {% if form.classcategory_id2 is defined %}
  342.                                     <div class="product-details__info__color product-details__info__item">
  343.                                         <div class="product-details__info__dt">規格</div>
  344.                                         <div class="form__input quantity">
  345.                                             {{ form_row(form.classcategory_id2) }}
  346.                                             {{ form_errors(form.classcategory_id2) }}
  347.                                         </div>
  348.                                     </div>
  349.                                 {% endif %}
  350.                             {% endif %}
  351.                             <div class="product-details__info__quantity product-details__info__item">
  352.                                 <div class="product-details__info__dt">{{ '数量'|trans }}</div>
  353.                                 <label class="number-spinner-wrap">
  354.                                     {{ form_widget(form.quantity) }}
  355.                                     {{ form_errors(form.quantity) }}
  356.                                     <div class="spinner__num-wrap">
  357.                                         <span class="spinner spinner-down">-</span>
  358.                                         <span class="spinner spinner-up">+</span>
  359.                                     </div>
  360.                                 </label>
  361.                             </div>
  362.                             <div class="product-details__btn__wrap">
  363.                                 <div class="common__btn__wrap">
  364.                                     <div class="common__btn">
  365.                                         <button type="submit" class="add-cart common__btn__inner common__btn__inner__cart">{{ 'カートに入れる'|trans }}</button>
  366.                                     </div>
  367.                                 </div>
  368.                             </div>
  369.                         {% else %}
  370.                             <button type="button" class="ec-blockBtn--action" disabled="disabled">
  371.                                 {{ 'ただいま品切れ中です。'|trans }}
  372.                             </button>
  373.                         {% endif %}
  374.                         {{ form_rest(form) }}
  375.                     </form>
  376.                     {% if BaseInfo.option_favorite_product %}
  377.                         <form action="{{ url('product_add_favorite', {id:Product.id}) }}" method="post">
  378.                             {% if is_favorite == false %}
  379.                                 <div class="common__btn__wrap">
  380.                                     <div class="common__btn">
  381.                                         <button type="submit" id="favorite" class="common__btn__inner">{{ 'お気に入りに追加'|trans }}</button>
  382.                                     </div>
  383.                                 </div>
  384.                             {% else %}
  385.                                 <div class="common__btn__wrap">
  386.                                     <div class="common__btn">
  387.                                         <button type="submit" id="favorite" class="common__btn__inner" disabled="disabled">{{ 'お気に入りに追加済です。'|trans }}</button>
  388.                                     </div>
  389.                                 </div>
  390.                             {% endif %}
  391.                         </form>
  392.                     {% endif %}
  393.                     <div class="ec-modal">
  394.                         <div class="ec-modal-overlay">
  395.                             <div class="ec-modal-wrap">
  396.                                 <span class="ec-modal-close"><span class="ec-icon"><img src="{{ asset('assets/icon/cross-dark.svg') }}" alt=""/></span></span>
  397.                                 <div id="ec-modal-header" class="text-center">{{ 'カートに追加しました。'|trans }}</div>
  398.                                 <div class="ec-modal-box">
  399.                                     <div class="ec-role">
  400.                                         <span class="ec-inlineBtn--cancel">{{ 'お買い物を続ける'|trans }}</span>
  401.                                         <a href="{{ url('cart') }}" class="ec-inlineBtn--action">{{ 'カートへ進む'|trans }}</a>
  402.                                     </div>
  403.                                 </div>
  404.                             </div>
  405.                         </div>
  406.                     </div>
  407.                 </div>
  408.             </section>
  409.             <div class="explanation explanation-sp">
  410.                 <ul class="explanation__info">
  411.                     <li>
  412.                         <span class="explanation__info__ttl"></span>
  413.                         <span class="explanation__info__txt">
  414.                             {{ Product.description_detail|raw|nl2br }}
  415.                         </span>
  416.                     </li>
  417.                 </ul>
  418.             </div>
  419.             {% set positive_avg_star = ProductReviewAvg %}
  420.             {% set negative_avg_star = 5 - positive_avg_star %}
  421.             <div class="review">
  422.                 <div class="review__mv__wrap">
  423.                     <div class="review__mv">{{ 'product_review.front.product_detail.title'|trans }}</div>
  424.                     <div class="review__mv__star__wrap">
  425.                         <div class="review__mv__star"><span class="star-w">{{ stars.stars(positive_avg_star, negative_avg_star) }}</span>
  426.                         </div>
  427.                         <div class="review__mv__star__num">({{ ProductReviewCount }})</div>
  428.                     </div>
  429.                 </div>
  430.                 {% if ProductReviews %}
  431.                     {% for ProductReview in ProductReviews %}
  432.                         <div class="review__time__wrap">
  433.                             <div class="review__time">・{{ ProductReview.create_date|date_day }}</div>
  434.                             {% if ProductReview.reviewer_url %}
  435.                                 <div class="review__name">
  436.                                     <a href="{{ ProductReview.reviewer_url }}" rel="ugc nofollow"
  437.                                     target="_blank">{{ 'product_review.front.product_detail.name'|trans({ '%name%': ProductReview.reviewer_name }) }}</a>
  438.                                 </div>
  439.                             {% else %}
  440.                                 <div class="review__name">
  441.                                     {{ 'product_review.front.product_detail.name'|trans({ '%name%': ProductReview.reviewer_name }) }}
  442.                                 </div>
  443.                             {% endif %}
  444.                             {% set positive_star = ProductReview.recommend_level %}
  445.                             {% set negative_star = 5 - positive_star %}
  446.                             <div class="review__time__star"><span class="star-b">★★★★</span><span class="star-b-n">{{ stars.stars(positive_star, negative_star) }}</span>
  447.                             </div>
  448.                         </div>
  449.                         <div class="review__ttl">{{ ProductReview.title }}</div>
  450.                         <div class="review__txt">
  451.                             {{ ProductReview.comment|nl2br }}
  452.                         </div>
  453.                     {% endfor %}
  454.                 {% else %}
  455.                     <p style="margin-top: 2rem;">{{ 'product_review.front.product_detail.no_review'|trans }}</p>
  456.                 {% endif %}
  457.             </div>
  458.             <div class="review__btn" style="margin-bottom: 2rem;"><a href="{{ url('product_review_index', { id: Product.id }) }}">{{ 'product_review.front.product_detail.post_review'|trans }}</a></div>
  459.         </div>
  460.     </div>
  461. {% endblock %}