From 991710595185e9ba35dd608724c29c5cf0e3858b Mon Sep 17 00:00:00 2001 From: magdev Date: Sat, 28 Feb 2026 10:28:13 +0100 Subject: [PATCH] Implement Phase 3: single product page templates (Bootstrap 5) Add 21 Twig template overrides for the single product page: Product layout: - product-image: gallery with thumbnail strip, img-fluid rounded - title: h1 entry-title - price: fs-3 fw-bold with sale del/ins markup - short-description: lead text-body-secondary - meta: dl row with SKU, categories, tags - rating: Bootstrap Icon stars with half-star, review count link - stock: badge (bg-success/bg-danger/bg-warning) per status - sale-flash: badge bg-danger fs-6 - share: hook-only wrapper - product-attributes: table-sm table-striped Related/upsells: - related, up-sells: section with product loop grid Tabs: - tabs: nav-tabs + tab-content with fade transitions - description: tab-pane with prose content - additional-information: tab-pane with attributes hook Add to cart (4 product types + variation JS): - simple: input-group quantity + btn-primary btn-lg - variable: form-select per attribute + variation display - grouped: table-borderless with quantity per child product - external: btn-outline-primary with external link icon - variation: Underscore.js script templates (Bootstrap-styled) - variation-add-to-cart-button: quantity + submit with hidden fields CSS additions: gallery thumbnail hover, variation selector spacing. Co-Authored-By: Claude Opus 4.6 --- PLAN.md | 42 ++++----- assets/css/wc-bootstrap.css | 34 +++++++ .../add-to-cart/external.html.twig | 32 +++++++ .../add-to-cart/grouped.html.twig | 84 ++++++++++++++++++ .../add-to-cart/simple.html.twig | 53 +++++++++++ .../add-to-cart/variable.html.twig | 88 +++++++++++++++++++ .../variation-add-to-cart-button.html.twig | 51 +++++++++++ .../add-to-cart/variation.html.twig | 30 +++++++ templates/single-product/meta.html.twig | 41 +++++++++ templates/single-product/price.html.twig | 16 ++++ .../product-attributes.html.twig | 32 +++++++ .../single-product/product-image.html.twig | 58 ++++++++++++ templates/single-product/rating.html.twig | 40 +++++++++ templates/single-product/related.html.twig | 32 +++++++ templates/single-product/sale-flash.html.twig | 20 +++++ templates/single-product/share.html.twig | 12 +++ .../short-description.html.twig | 17 ++++ templates/single-product/stock.html.twig | 31 +++++++ .../tabs/additional-information.html.twig | 22 +++++ .../single-product/tabs/description.html.twig | 26 ++++++ templates/single-product/tabs/tabs.html.twig | 56 ++++++++++++ templates/single-product/title.html.twig | 15 ++++ templates/single-product/up-sells.html.twig | 32 +++++++ 23 files changed, 843 insertions(+), 21 deletions(-) create mode 100644 templates/single-product/add-to-cart/external.html.twig create mode 100644 templates/single-product/add-to-cart/grouped.html.twig create mode 100644 templates/single-product/add-to-cart/simple.html.twig create mode 100644 templates/single-product/add-to-cart/variable.html.twig create mode 100644 templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig create mode 100644 templates/single-product/add-to-cart/variation.html.twig create mode 100644 templates/single-product/meta.html.twig create mode 100644 templates/single-product/price.html.twig create mode 100644 templates/single-product/product-attributes.html.twig create mode 100644 templates/single-product/product-image.html.twig create mode 100644 templates/single-product/rating.html.twig create mode 100644 templates/single-product/related.html.twig create mode 100644 templates/single-product/sale-flash.html.twig create mode 100644 templates/single-product/share.html.twig create mode 100644 templates/single-product/short-description.html.twig create mode 100644 templates/single-product/stock.html.twig create mode 100644 templates/single-product/tabs/additional-information.html.twig create mode 100644 templates/single-product/tabs/description.html.twig create mode 100644 templates/single-product/tabs/tabs.html.twig create mode 100644 templates/single-product/title.html.twig create mode 100644 templates/single-product/up-sells.html.twig diff --git a/PLAN.md b/PLAN.md index 5b26627..f33456c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -549,27 +549,27 @@ Track completion per file. Mark with `[x]` when done. ### Phase 3 -- Single Product -- [ ] `single-product/product-image.html.twig` -- [ ] `single-product/title.html.twig` -- [ ] `single-product/price.html.twig` -- [ ] `single-product/short-description.html.twig` -- [ ] `single-product/meta.html.twig` -- [ ] `single-product/rating.html.twig` -- [ ] `single-product/stock.html.twig` -- [ ] `single-product/sale-flash.html.twig` -- [ ] `single-product/share.html.twig` -- [ ] `single-product/product-attributes.html.twig` -- [ ] `single-product/related.html.twig` -- [ ] `single-product/up-sells.html.twig` -- [ ] `single-product/add-to-cart/simple.html.twig` -- [ ] `single-product/add-to-cart/variable.html.twig` -- [ ] `single-product/add-to-cart/grouped.html.twig` -- [ ] `single-product/add-to-cart/external.html.twig` -- [ ] `single-product/add-to-cart/variation.html.twig` -- [ ] `single-product/add-to-cart/variation-add-to-cart-button.html.twig` -- [ ] `single-product/tabs/tabs.html.twig` -- [ ] `single-product/tabs/description.html.twig` -- [ ] `single-product/tabs/additional-information.html.twig` +- [x] `single-product/product-image.html.twig` +- [x] `single-product/title.html.twig` +- [x] `single-product/price.html.twig` +- [x] `single-product/short-description.html.twig` +- [x] `single-product/meta.html.twig` +- [x] `single-product/rating.html.twig` +- [x] `single-product/stock.html.twig` +- [x] `single-product/sale-flash.html.twig` +- [x] `single-product/share.html.twig` +- [x] `single-product/product-attributes.html.twig` +- [x] `single-product/related.html.twig` +- [x] `single-product/up-sells.html.twig` +- [x] `single-product/add-to-cart/simple.html.twig` +- [x] `single-product/add-to-cart/variable.html.twig` +- [x] `single-product/add-to-cart/grouped.html.twig` +- [x] `single-product/add-to-cart/external.html.twig` +- [x] `single-product/add-to-cart/variation.html.twig` +- [x] `single-product/add-to-cart/variation-add-to-cart-button.html.twig` +- [x] `single-product/tabs/tabs.html.twig` +- [x] `single-product/tabs/description.html.twig` +- [x] `single-product/tabs/additional-information.html.twig` ### Phase 4 -- Cart diff --git a/assets/css/wc-bootstrap.css b/assets/css/wc-bootstrap.css index ee33114..da10b58 100644 --- a/assets/css/wc-bootstrap.css +++ b/assets/css/wc-bootstrap.css @@ -144,6 +144,40 @@ font-weight: 600; } +/* ========================================================================== + Product Gallery + Thumbnail grid and cursor for single product image gallery. + ========================================================================== */ + +.wc-gallery-thumb { + cursor: pointer; + opacity: 0.7; + transition: opacity 0.15s ease; +} + +.wc-gallery-thumb:hover { + opacity: 1; +} + +.woocommerce-product-gallery__image img { + width: 100%; + height: auto; + border-radius: var(--bs-border-radius); +} + +/* ========================================================================== + Variation Selectors + Spacing for variable product attribute dropdowns. + ========================================================================== */ + +.variations_form .reset_variations { + font-size: 0.875rem; +} + +.single_variation_wrap .woocommerce-variation { + margin-bottom: 1rem; +} + /* ========================================================================== WooCommerce Grid Override Reset WooCommerce's default grid to let Bootstrap handle layout. diff --git a/templates/single-product/add-to-cart/external.html.twig b/templates/single-product/add-to-cart/external.html.twig new file mode 100644 index 0000000..722473f --- /dev/null +++ b/templates/single-product/add-to-cart/external.html.twig @@ -0,0 +1,32 @@ +{# + # External/Affiliate Product Add to Cart (Bootstrap 5 Override) + # + # Renders a link button to the external product URL. + # + # Expected context: + # product_url - External product URL + # button_text - Button label text + # + # WooCommerce PHP equivalent: single-product/add-to-cart/external.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{{ do_action('woocommerce_before_add_to_cart_form') }} + +
+ {{ do_action('woocommerce_before_add_to_cart_button') }} + + + {{ button_text|default(__('Buy product'))|esc_html }} + + + + {{ do_action('woocommerce_after_add_to_cart_button') }} +
+ +{{ do_action('woocommerce_after_add_to_cart_form') }} diff --git a/templates/single-product/add-to-cart/grouped.html.twig b/templates/single-product/add-to-cart/grouped.html.twig new file mode 100644 index 0000000..61efdea --- /dev/null +++ b/templates/single-product/add-to-cart/grouped.html.twig @@ -0,0 +1,84 @@ +{# + # Grouped Product Add to Cart (Bootstrap 5 Override) + # + # Add-to-cart form for grouped products: table of child products with quantities. + # + # Expected context: + # product - WC_Product_Grouped object + # grouped_products - Array of child WC_Product objects + # grouped_product_columns - Array of column definitions + # quantites_required - Whether quantities are required + # show_add_to_cart_button - Whether to show the submit button + # + # WooCommerce PHP equivalent: single-product/add-to-cart/grouped.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{{ do_action('woocommerce_before_add_to_cart_form') }} + +
+
+ + {{ do_action('woocommerce_grouped_product_list_before') }} + + {% for grouped_product in grouped_products %} + {% set child_id = grouped_product.get_id() %} + + + + + + + + {% endfor %} + + {{ do_action('woocommerce_grouped_product_list_after') }} +
+ {% if grouped_product.is_purchasable() and grouped_product.is_in_stock() %} + {% include 'global/quantity-input.html.twig' with { + input_id: 'quantity_' ~ child_id, + input_name: 'quantity[' ~ child_id ~ ']', + input_value: 0, + min_value: 0, + max_value: grouped_product.get_max_purchase_quantity()|default(0), + step: 1, + placeholder: '0', + inputmode: 'numeric', + classes: 'qty', + readonly: false, + type: 'number', + args: { product_name: grouped_product.get_name() } + } %} + {% endif %} + + + + + {{ grouped_product.get_price_html()|raw }} + +
+
+ + {% if show_add_to_cart_button is not defined or show_add_to_cart_button %} + {{ do_action('woocommerce_before_add_to_cart_button') }} + + + + + {{ do_action('woocommerce_after_add_to_cart_button') }} + {% endif %} +
+ +{{ do_action('woocommerce_after_add_to_cart_form') }} diff --git a/templates/single-product/add-to-cart/simple.html.twig b/templates/single-product/add-to-cart/simple.html.twig new file mode 100644 index 0000000..b94db2e --- /dev/null +++ b/templates/single-product/add-to-cart/simple.html.twig @@ -0,0 +1,53 @@ +{# + # Simple Product Add to Cart (Bootstrap 5 Override) + # + # Add-to-cart form for simple products: quantity input + button. + # + # Expected context: + # product - WC_Product_Simple object + # + # WooCommerce PHP equivalent: single-product/add-to-cart/simple.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product.is_purchasable() and product.is_in_stock() %} + {{ do_action('woocommerce_before_add_to_cart_form') }} + +
+ {{ do_action('woocommerce_before_add_to_cart_button') }} + +
+ {{ do_action('woocommerce_before_add_to_cart_quantity') }} + + {% include 'global/quantity-input.html.twig' with { + input_id: 'quantity_' ~ product.get_id(), + input_name: 'quantity', + input_value: 1, + min_value: product.get_min_purchase_quantity()|default(1), + max_value: product.get_max_purchase_quantity()|default(0), + step: 1, + placeholder: '', + inputmode: 'numeric', + classes: 'qty', + readonly: false, + type: 'number', + args: { product_name: product.get_name() } + } %} + + {{ do_action('woocommerce_after_add_to_cart_quantity') }} + + +
+ + {{ do_action('woocommerce_after_add_to_cart_button') }} +
+ + {{ do_action('woocommerce_after_add_to_cart_form') }} +{% endif %} diff --git a/templates/single-product/add-to-cart/variable.html.twig b/templates/single-product/add-to-cart/variable.html.twig new file mode 100644 index 0000000..6d28fbf --- /dev/null +++ b/templates/single-product/add-to-cart/variable.html.twig @@ -0,0 +1,88 @@ +{# + # Variable Product Add to Cart (Bootstrap 5 Override) + # + # Add-to-cart form for variable products with attribute selectors. + # + # Expected context: + # product - WC_Product_Variable object + # attributes - Array of attribute taxonomies + # available_variations - JSON-encoded variations data + # attribute_keys - Array of attribute keys + # variations_json - Variations data as JSON string + # variations_attr - Data attribute string for the form + # selected_attributes - Currently selected attribute values + # + # WooCommerce PHP equivalent: single-product/add-to-cart/variable.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{{ do_action('woocommerce_before_add_to_cart_form') }} + +
+ + {{ do_action('woocommerce_before_variations_form') }} + + {% if available_variations is defined %} + {# Variation attribute selectors #} +
+ {% for attribute_name, options in attributes %} +
+ + +
+ {% endfor %} +
+ + {{ do_action('woocommerce_after_variations_table') }} + + {# Reset link #} + + + {# Single variation display + add-to-cart button #} +
+ {{ do_action('woocommerce_before_single_variation') }} + +
+ +
+ {{ do_action('woocommerce_single_variation') }} +
+ + {{ do_action('woocommerce_after_single_variation') }} +
+ {% else %} + {# Out of stock / unavailable #} +

+ {{ __('This product is currently out of stock and unavailable.') }} +

+ {% endif %} + + {{ do_action('woocommerce_after_variations_form') }} +
+ +{{ do_action('woocommerce_after_add_to_cart_form') }} diff --git a/templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig b/templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig new file mode 100644 index 0000000..65f4360 --- /dev/null +++ b/templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig @@ -0,0 +1,51 @@ +{# + # Variation Add to Cart Button (Bootstrap 5 Override) + # + # Renders the quantity input and add-to-cart button for variable products + # after variation selection. + # + # Expected context: + # product - WC_Product_Variable object + # + # WooCommerce PHP equivalent: single-product/add-to-cart/variation-add-to-cart-button.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+ {{ do_action('woocommerce_before_add_to_cart_button') }} + +
+ {{ do_action('woocommerce_before_add_to_cart_quantity') }} + + {% include 'global/quantity-input.html.twig' with { + input_id: 'quantity_' ~ product.get_id(), + input_name: 'quantity', + input_value: 1, + min_value: product.get_min_purchase_quantity()|default(1), + max_value: product.get_max_purchase_quantity()|default(0), + step: 1, + placeholder: '', + inputmode: 'numeric', + classes: 'qty', + readonly: false, + type: 'number', + args: { product_name: product.get_name() } + } %} + + {{ do_action('woocommerce_after_add_to_cart_quantity') }} + + +
+ + + + + + {{ do_action('woocommerce_after_add_to_cart_button') }} +
diff --git a/templates/single-product/add-to-cart/variation.html.twig b/templates/single-product/add-to-cart/variation.html.twig new file mode 100644 index 0000000..0e5e800 --- /dev/null +++ b/templates/single-product/add-to-cart/variation.html.twig @@ -0,0 +1,30 @@ +{# + # Variation Templates (Bootstrap 5 Override) + # + # JavaScript templates (Underscore.js syntax) used by WooCommerce to render + # variation details dynamically when a user selects product attributes. + # These are script templates, not rendered server-side. + # + # WooCommerce PHP equivalent: single-product/add-to-cart/variation.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + + + + diff --git a/templates/single-product/meta.html.twig b/templates/single-product/meta.html.twig new file mode 100644 index 0000000..6f83da7 --- /dev/null +++ b/templates/single-product/meta.html.twig @@ -0,0 +1,41 @@ +{# + # Product Meta (Bootstrap 5 Override) + # + # Renders SKU, categories, and tags as a definition list. + # + # Expected context: + # product - WC_Product object with: + # .get_sku() - SKU string + # .get_id() - Product ID + # sku - SKU string (fallback) + # categories_html - Pre-rendered category links HTML + # tags_html - Pre-rendered tag links HTML + # + # WooCommerce PHP equivalent: single-product/meta.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+ {{ do_action('woocommerce_product_meta_start') }} + +
+ {% if product.get_sku() is defined and product.get_sku() %} +
{{ __('SKU:') }}
+
{{ product.get_sku()|esc_html }}
+ {% endif %} + + {% if categories_html is defined and categories_html %} +
{{ __('Categories:') }}
+
{{ categories_html|raw }}
+ {% endif %} + + {% if tags_html is defined and tags_html %} +
{{ __('Tags:') }}
+
{{ tags_html|raw }}
+ {% endif %} +
+ + {{ do_action('woocommerce_product_meta_end') }} +
diff --git a/templates/single-product/price.html.twig b/templates/single-product/price.html.twig new file mode 100644 index 0000000..f740cfa --- /dev/null +++ b/templates/single-product/price.html.twig @@ -0,0 +1,16 @@ +{# + # Product Price (Bootstrap 5 Override) + # + # Expected context: + # product - WC_Product object with: + # .get_price_html() - Formatted price HTML + # + # WooCommerce PHP equivalent: single-product/price.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +

+ {{ product.get_price_html()|raw }} +

diff --git a/templates/single-product/product-attributes.html.twig b/templates/single-product/product-attributes.html.twig new file mode 100644 index 0000000..f9a4e4f --- /dev/null +++ b/templates/single-product/product-attributes.html.twig @@ -0,0 +1,32 @@ +{# + # Product Attributes Table (Bootstrap 5 Override) + # + # Renders product attributes as a Bootstrap striped table. + # + # Expected context: + # product_attributes - Array of attribute objects, each with: + # .label - Attribute label + # .value - Attribute value (HTML) + # + # WooCommerce PHP equivalent: single-product/product-attributes.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product_attributes is defined and product_attributes|length > 0 %} + + + {% for attribute in product_attributes %} + + + + + {% endfor %} + +
+ {{ attribute.label|esc_html }} + + {{ attribute.value|raw }} +
+{% endif %} diff --git a/templates/single-product/product-image.html.twig b/templates/single-product/product-image.html.twig new file mode 100644 index 0000000..80f7240 --- /dev/null +++ b/templates/single-product/product-image.html.twig @@ -0,0 +1,58 @@ +{# + # Product Image Gallery (Bootstrap 5 Override) + # + # Renders the product image gallery with main image and thumbnail strip. + # + # Expected context: + # product - WC_Product object + # post_thumbnail_id - Main image attachment ID + # columns - Number of thumbnail columns + # gallery_image_ids - Array of gallery attachment IDs + # main_image_html - Pre-rendered main image HTML + # + # WooCommerce PHP equivalent: single-product/product-image.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set gallery_classes = 'woocommerce-product-gallery' %} + + diff --git a/templates/single-product/rating.html.twig b/templates/single-product/rating.html.twig new file mode 100644 index 0000000..e35fcad --- /dev/null +++ b/templates/single-product/rating.html.twig @@ -0,0 +1,40 @@ +{# + # Product Rating (Bootstrap 5 Override) + # + # Renders the star rating with review count link on the single product page. + # + # Expected context: + # product - WC_Product object + # rating_count - Number of ratings + # review_count - Number of reviews + # average - Average rating (0-5) + # + # WooCommerce PHP equivalent: single-product/rating.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if rating_count is defined and rating_count > 0 %} +
+ + + {% if review_count is defined and review_count > 0 %} + + {{ _n('%s customer review', '%s customer reviews', review_count)|format(review_count) }} + + {% endif %} +
+{% endif %} diff --git a/templates/single-product/related.html.twig b/templates/single-product/related.html.twig new file mode 100644 index 0000000..9b8f14b --- /dev/null +++ b/templates/single-product/related.html.twig @@ -0,0 +1,32 @@ +{# + # Related Products (Bootstrap 5 Override) + # + # Renders the related products section below the single product. + # + # Expected context: + # related_products - Array of WC_Product objects + # columns - Number of columns for the grid + # heading - Section heading text (filtered) + # + # WooCommerce PHP equivalent: single-product/related.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if related_products is defined and related_products|length > 0 %} + +{% endif %} diff --git a/templates/single-product/sale-flash.html.twig b/templates/single-product/sale-flash.html.twig new file mode 100644 index 0000000..5f2fea9 --- /dev/null +++ b/templates/single-product/sale-flash.html.twig @@ -0,0 +1,20 @@ +{# + # Sale Flash Badge (Bootstrap 5 Override) + # + # Renders the sale badge on the single product page. + # + # Expected context: + # product - WC_Product object + # post - Global post object + # + # WooCommerce PHP equivalent: single-product/sale-flash.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product is defined and product.is_on_sale() %} + + {{ __('Sale!') }} + +{% endif %} diff --git a/templates/single-product/share.html.twig b/templates/single-product/share.html.twig new file mode 100644 index 0000000..0a5af36 --- /dev/null +++ b/templates/single-product/share.html.twig @@ -0,0 +1,12 @@ +{# + # Social Share Buttons (Bootstrap 5 Override) + # + # Renders social sharing buttons. Content is injected via hook. + # + # WooCommerce PHP equivalent: single-product/share.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{{ do_action('woocommerce_share') }} diff --git a/templates/single-product/short-description.html.twig b/templates/single-product/short-description.html.twig new file mode 100644 index 0000000..128037a --- /dev/null +++ b/templates/single-product/short-description.html.twig @@ -0,0 +1,17 @@ +{# + # Product Short Description (Bootstrap 5 Override) + # + # Expected context: + # short_description - Product short description HTML + # + # WooCommerce PHP equivalent: single-product/short-description.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if short_description is defined and short_description %} +
+ {{ short_description|raw }} +
+{% endif %} diff --git a/templates/single-product/stock.html.twig b/templates/single-product/stock.html.twig new file mode 100644 index 0000000..eab0902 --- /dev/null +++ b/templates/single-product/stock.html.twig @@ -0,0 +1,31 @@ +{# + # Stock Status (Bootstrap 5 Override) + # + # Renders the product stock status as a Bootstrap badge. + # + # Expected context: + # class - Stock CSS class ('in-stock', 'out-of-stock', 'on-backorder') + # availability - Stock availability text + # + # WooCommerce PHP equivalent: single-product/stock.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if availability is defined and availability %} + {% set badge_class = 'bg-secondary' %} + {% if class is defined %} + {% if 'in-stock' in class %} + {% set badge_class = 'bg-success' %} + {% elseif 'out-of-stock' in class %} + {% set badge_class = 'bg-danger' %} + {% elseif 'on-backorder' in class %} + {% set badge_class = 'bg-warning text-dark' %} + {% endif %} + {% endif %} + +

+ {{ availability|esc_html }} +

+{% endif %} diff --git a/templates/single-product/tabs/additional-information.html.twig b/templates/single-product/tabs/additional-information.html.twig new file mode 100644 index 0000000..da30767 --- /dev/null +++ b/templates/single-product/tabs/additional-information.html.twig @@ -0,0 +1,22 @@ +{# + # Additional Information Tab Content (Bootstrap 5 Override) + # + # Renders the product attributes table inside the Additional Information tab. + # + # Expected context: + # product - WC_Product object + # heading - Tab heading text (filtered) + # + # WooCommerce PHP equivalent: single-product/tabs/additional-information.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set heading = heading|default(__('Additional information')) %} + +{% if heading %} +

{{ heading|esc_html }}

+{% endif %} + +{{ do_action('woocommerce_product_additional_information', product) }} diff --git a/templates/single-product/tabs/description.html.twig b/templates/single-product/tabs/description.html.twig new file mode 100644 index 0000000..5ca939b --- /dev/null +++ b/templates/single-product/tabs/description.html.twig @@ -0,0 +1,26 @@ +{# + # Description Tab Content (Bootstrap 5 Override) + # + # Renders the product description inside the Description tab pane. + # + # Expected context: + # heading - Tab heading text (filtered) + # description - Product description HTML (the_content()) + # + # WooCommerce PHP equivalent: single-product/tabs/description.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set heading = heading|default(__('Description')) %} + +{% if heading %} +

{{ heading|esc_html }}

+{% endif %} + +{% if description is defined %} + {{ description|raw }} +{% else %} + {{ the_content() }} +{% endif %} diff --git a/templates/single-product/tabs/tabs.html.twig b/templates/single-product/tabs/tabs.html.twig new file mode 100644 index 0000000..e22b4b0 --- /dev/null +++ b/templates/single-product/tabs/tabs.html.twig @@ -0,0 +1,56 @@ +{# + # Product Tabs (Bootstrap 5 Override) + # + # Renders product tabs (Description, Additional Info, Reviews) using + # Bootstrap 5 nav-tabs and tab-content with fade transitions. + # + # Expected context: + # product_tabs - Associative array of tabs, each with: + # key.title - Tab title + # key.callback - Tab content callback name + # key.priority - Sort order + # + # WooCommerce PHP equivalent: single-product/tabs/tabs.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product_tabs is defined and product_tabs|length > 0 %} +
+ {# Tab navigation #} + + + {# Tab content panels #} +
+ {% for key, tab in product_tabs %} +
+ {% if tab.callback is defined %} + {{ call_user_func(tab.callback, key, tab) }} + {% endif %} +
+ {% endfor %} +
+ + {{ do_action('woocommerce_product_after_tabs') }} +
+{% endif %} diff --git a/templates/single-product/title.html.twig b/templates/single-product/title.html.twig new file mode 100644 index 0000000..127e321 --- /dev/null +++ b/templates/single-product/title.html.twig @@ -0,0 +1,15 @@ +{# + # Product Title (Bootstrap 5 Override) + # + # Expected context: + # product - WC_Product object (or uses the_title()) + # + # WooCommerce PHP equivalent: single-product/title.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +

+ {{ the_title() }} +

diff --git a/templates/single-product/up-sells.html.twig b/templates/single-product/up-sells.html.twig new file mode 100644 index 0000000..10105df --- /dev/null +++ b/templates/single-product/up-sells.html.twig @@ -0,0 +1,32 @@ +{# + # Upsell Products (Bootstrap 5 Override) + # + # Renders the upsell products section below the single product. + # + # Expected context: + # upsells - Array of WC_Product objects + # columns - Number of columns for the grid + # heading - Section heading text (filtered) + # + # WooCommerce PHP equivalent: single-product/up-sells.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if upsells is defined and upsells|length > 0 %} +
+ {% set heading = heading|default(__('You may also like…')) %} + {% if heading %} +

{{ heading }}

+ {% endif %} + + {{ woocommerce_product_loop_start() }} + + {% for product in upsells %} + {% include 'content-product.html.twig' with { product: product } %} + {% endfor %} + + {{ woocommerce_product_loop_end() }} +
+{% endif %}