You've already forked wc-bootstrap
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 <noreply@anthropic.com>
This commit is contained in:
42
PLAN.md
42
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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
32
templates/single-product/add-to-cart/external.html.twig
Normal file
32
templates/single-product/add-to-cart/external.html.twig
Normal file
@@ -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') }}
|
||||
|
||||
<form class="cart" action="{{ product_url|default('#')|esc_url }}" method="get">
|
||||
{{ do_action('woocommerce_before_add_to_cart_button') }}
|
||||
|
||||
<a href="{{ product_url|default('#')|esc_url }}"
|
||||
class="btn btn-outline-primary btn-lg single_add_to_cart_button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow">
|
||||
{{ button_text|default(__('Buy product'))|esc_html }}
|
||||
<i class="bi bi-box-arrow-up-right ms-2" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_button') }}
|
||||
</form>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_form') }}
|
||||
84
templates/single-product/add-to-cart/grouped.html.twig
Normal file
84
templates/single-product/add-to-cart/grouped.html.twig
Normal file
@@ -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') }}
|
||||
|
||||
<form class="cart grouped_form" action="{{ product.get_permalink()|esc_url }}" method="post" enctype="multipart/form-data">
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="woocommerce-grouped-product-list group_table table table-borderless align-middle">
|
||||
{{ do_action('woocommerce_grouped_product_list_before') }}
|
||||
|
||||
{% for grouped_product in grouped_products %}
|
||||
{% set child_id = grouped_product.get_id() %}
|
||||
<tr id="product-{{ child_id }}" class="{{ grouped_product.get_stock_status() }}">
|
||||
<td class="woocommerce-grouped-product-list-item__quantity" style="width: 140px;">
|
||||
{% 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 %}
|
||||
</td>
|
||||
|
||||
<td class="woocommerce-grouped-product-list-item__label">
|
||||
<label for="quantity_{{ child_id }}">
|
||||
{% if grouped_product.is_visible() %}
|
||||
<a href="{{ grouped_product.get_permalink()|esc_url }}" class="text-decoration-none">
|
||||
{{ grouped_product.get_name()|esc_html }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ grouped_product.get_name()|esc_html }}
|
||||
{% endif %}
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="woocommerce-grouped-product-list-item__price text-end">
|
||||
<span class="price">
|
||||
{{ grouped_product.get_price_html()|raw }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{{ do_action('woocommerce_grouped_product_list_after') }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if show_add_to_cart_button is not defined or show_add_to_cart_button %}
|
||||
{{ do_action('woocommerce_before_add_to_cart_button') }}
|
||||
|
||||
<input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
|
||||
<button type="submit" class="btn btn-primary btn-lg single_add_to_cart_button">
|
||||
{{ product.single_add_to_cart_text() }}
|
||||
</button>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_button') }}
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_form') }}
|
||||
53
templates/single-product/add-to-cart/simple.html.twig
Normal file
53
templates/single-product/add-to-cart/simple.html.twig
Normal file
@@ -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') }}
|
||||
|
||||
<form class="cart" action="{{ product.get_permalink()|esc_url }}" method="post" enctype="multipart/form-data">
|
||||
{{ do_action('woocommerce_before_add_to_cart_button') }}
|
||||
|
||||
<div class="d-flex align-items-end gap-3 mb-3">
|
||||
{{ 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') }}
|
||||
|
||||
<button type="submit"
|
||||
name="add-to-cart"
|
||||
value="{{ product.get_id() }}"
|
||||
class="btn btn-primary btn-lg single_add_to_cart_button">
|
||||
{{ product.single_add_to_cart_text() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_button') }}
|
||||
</form>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_form') }}
|
||||
{% endif %}
|
||||
88
templates/single-product/add-to-cart/variable.html.twig
Normal file
88
templates/single-product/add-to-cart/variable.html.twig
Normal file
@@ -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') }}
|
||||
|
||||
<form class="variations_form cart"
|
||||
action="{{ product.get_permalink()|esc_url }}"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
{{ variations_attr|default('')|raw }}>
|
||||
|
||||
{{ do_action('woocommerce_before_variations_form') }}
|
||||
|
||||
{% if available_variations is defined %}
|
||||
{# Variation attribute selectors #}
|
||||
<div class="variations mb-4">
|
||||
{% for attribute_name, options in attributes %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" for="{{ attribute_name|esc_attr }}">
|
||||
{{ wc_attribute_label(attribute_name)|esc_html }}
|
||||
</label>
|
||||
<select id="{{ attribute_name|esc_attr }}"
|
||||
class="form-select"
|
||||
name="attribute_{{ attribute_name|esc_attr }}"
|
||||
data-attribute_name="attribute_{{ attribute_name|esc_attr }}">
|
||||
<option value="">{{ __('Choose an option') }}</option>
|
||||
{% if options is iterable %}
|
||||
{% for option in options %}
|
||||
{% set selected_value = selected_attributes[attribute_name]|default('') %}
|
||||
<option value="{{ option|esc_attr }}"{% if option == selected_value %} selected{% endif %}>
|
||||
{{ option|esc_html }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_after_variations_table') }}
|
||||
|
||||
{# Reset link #}
|
||||
<div class="reset_variations_wrapper mb-3" style="display: none;">
|
||||
<a class="reset_variations btn btn-link btn-sm p-0 text-decoration-none" href="#">
|
||||
{{ __('Clear') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Single variation display + add-to-cart button #}
|
||||
<div class="single_variation_wrap">
|
||||
{{ do_action('woocommerce_before_single_variation') }}
|
||||
|
||||
<div class="woocommerce-variation single_variation"></div>
|
||||
|
||||
<div class="woocommerce-variation-add-to-cart variations_button">
|
||||
{{ do_action('woocommerce_single_variation') }}
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_after_single_variation') }}
|
||||
</div>
|
||||
{% else %}
|
||||
{# Out of stock / unavailable #}
|
||||
<p class="stock out-of-stock alert alert-warning">
|
||||
{{ __('This product is currently out of stock and unavailable.') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{{ do_action('woocommerce_after_variations_form') }}
|
||||
</form>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_form') }}
|
||||
@@ -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
|
||||
#}
|
||||
|
||||
<div class="woocommerce-variation-add-to-cart variations_button">
|
||||
{{ do_action('woocommerce_before_add_to_cart_button') }}
|
||||
|
||||
<div class="d-flex align-items-end gap-3 mb-3">
|
||||
{{ 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') }}
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-primary btn-lg single_add_to_cart_button"
|
||||
disabled>
|
||||
{{ product.single_add_to_cart_text() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
|
||||
<input type="hidden" name="product_id" value="{{ product.get_id() }}" />
|
||||
<input type="hidden" name="variation_id" class="variation_id" value="0" />
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_button') }}
|
||||
</div>
|
||||
30
templates/single-product/add-to-cart/variation.html.twig
Normal file
30
templates/single-product/add-to-cart/variation.html.twig
Normal file
@@ -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
|
||||
#}
|
||||
|
||||
<script type="text/template" id="tmpl-variation-template">
|
||||
<div class="woocommerce-variation-description mb-2">
|
||||
{{{ data.variation.variation_description }}}
|
||||
</div>
|
||||
<div class="woocommerce-variation-price mb-3">
|
||||
<span class="price fs-3 fw-bold">{{{ data.variation.price_html }}}</span>
|
||||
</div>
|
||||
<div class="woocommerce-variation-availability mb-2">
|
||||
{{{ data.variation.availability_html }}}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="tmpl-unavailable-variation-template">
|
||||
<p class="alert alert-warning mb-0">
|
||||
{{ __('Sorry, this product is unavailable. Please choose a different combination.') }}
|
||||
</p>
|
||||
</script>
|
||||
41
templates/single-product/meta.html.twig
Normal file
41
templates/single-product/meta.html.twig
Normal file
@@ -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
|
||||
#}
|
||||
|
||||
<div class="product_meta border-top pt-3 mt-3">
|
||||
{{ do_action('woocommerce_product_meta_start') }}
|
||||
|
||||
<dl class="row mb-0 small">
|
||||
{% if product.get_sku() is defined and product.get_sku() %}
|
||||
<dt class="col-sm-3 text-body-secondary">{{ __('SKU:') }}</dt>
|
||||
<dd class="col-sm-9">{{ product.get_sku()|esc_html }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if categories_html is defined and categories_html %}
|
||||
<dt class="col-sm-3 text-body-secondary">{{ __('Categories:') }}</dt>
|
||||
<dd class="col-sm-9">{{ categories_html|raw }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if tags_html is defined and tags_html %}
|
||||
<dt class="col-sm-3 text-body-secondary">{{ __('Tags:') }}</dt>
|
||||
<dd class="col-sm-9">{{ tags_html|raw }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
{{ do_action('woocommerce_product_meta_end') }}
|
||||
</div>
|
||||
16
templates/single-product/price.html.twig
Normal file
16
templates/single-product/price.html.twig
Normal file
@@ -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
|
||||
#}
|
||||
|
||||
<p class="price fs-3 fw-bold mb-3">
|
||||
{{ product.get_price_html()|raw }}
|
||||
</p>
|
||||
32
templates/single-product/product-attributes.html.twig
Normal file
32
templates/single-product/product-attributes.html.twig
Normal file
@@ -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 %}
|
||||
<table class="woocommerce-product-attributes table table-sm table-striped mb-0">
|
||||
<tbody>
|
||||
{% for attribute in product_attributes %}
|
||||
<tr class="woocommerce-product-attributes-item">
|
||||
<th class="woocommerce-product-attributes-item__label fw-semibold" scope="row">
|
||||
{{ attribute.label|esc_html }}
|
||||
</th>
|
||||
<td class="woocommerce-product-attributes-item__value">
|
||||
{{ attribute.value|raw }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
58
templates/single-product/product-image.html.twig
Normal file
58
templates/single-product/product-image.html.twig
Normal file
@@ -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' %}
|
||||
|
||||
<div class="{{ gallery_classes }}" data-columns="{{ columns|default(4) }}">
|
||||
<div class="woocommerce-product-gallery__wrapper">
|
||||
{# Main product image #}
|
||||
{% if main_image_html is defined and main_image_html %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
{{ main_image_html|raw }}
|
||||
</div>
|
||||
{% elseif post_thumbnail_id is defined and post_thumbnail_id %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
<img src="{{ wp_get_attachment_url(post_thumbnail_id)|esc_url }}"
|
||||
class="img-fluid rounded"
|
||||
alt="{{ product.get_name()|esc_attr }}" />
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
<img src="{{ wc_placeholder_img_src()|esc_url }}"
|
||||
class="img-fluid rounded"
|
||||
alt="{{ __('Placeholder')|esc_attr }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Thumbnail gallery strip #}
|
||||
{% if gallery_image_ids is defined and gallery_image_ids|length > 0 %}
|
||||
<div class="row row-cols-{{ columns|default(4) }} g-2">
|
||||
{% for image_id in gallery_image_ids %}
|
||||
<div class="col">
|
||||
<img src="{{ wp_get_attachment_url(image_id)|esc_url }}"
|
||||
class="img-fluid rounded border cursor-pointer wc-gallery-thumb"
|
||||
alt="{{ product.get_name()|esc_attr }}"
|
||||
data-full-src="{{ wp_get_attachment_url(image_id)|esc_url }}" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_product_thumbnails') }}
|
||||
</div>
|
||||
40
templates/single-product/rating.html.twig
Normal file
40
templates/single-product/rating.html.twig
Normal file
@@ -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 %}
|
||||
<div class="woocommerce-product-rating d-flex align-items-center gap-2 mb-3">
|
||||
<div class="wc-star-rating d-flex align-items-center gap-1"
|
||||
role="img"
|
||||
aria-label="{{ __('%s out of 5 stars')|format(average) }}">
|
||||
{% for i in 1..5 %}
|
||||
{% if i <= average|round(0, 'floor') %}
|
||||
<i class="bi bi-star-fill text-warning" aria-hidden="true"></i>
|
||||
{% elseif i - average < 1 %}
|
||||
<i class="bi bi-star-half text-warning" aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-star text-warning" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if review_count is defined and review_count > 0 %}
|
||||
<a href="#reviews" class="woocommerce-review-link text-body-secondary text-decoration-none small" rel="nofollow">
|
||||
{{ _n('%s customer review', '%s customer reviews', review_count)|format(review_count) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
32
templates/single-product/related.html.twig
Normal file
32
templates/single-product/related.html.twig
Normal file
@@ -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 %}
|
||||
<section class="related products mt-5">
|
||||
{% set heading = heading|default(__('Related products')) %}
|
||||
{% if heading %}
|
||||
<h2 class="h4 mb-4">{{ heading|esc_html }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{{ woocommerce_product_loop_start() }}
|
||||
|
||||
{% for product in related_products %}
|
||||
{% include 'content-product.html.twig' with { product: product } %}
|
||||
{% endfor %}
|
||||
|
||||
{{ woocommerce_product_loop_end() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
20
templates/single-product/sale-flash.html.twig
Normal file
20
templates/single-product/sale-flash.html.twig
Normal file
@@ -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() %}
|
||||
<span class="badge bg-danger fs-6 onsale">
|
||||
{{ __('Sale!') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
12
templates/single-product/share.html.twig
Normal file
12
templates/single-product/share.html.twig
Normal file
@@ -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') }}
|
||||
17
templates/single-product/short-description.html.twig
Normal file
17
templates/single-product/short-description.html.twig
Normal file
@@ -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 %}
|
||||
<div class="woocommerce-product-details__short-description lead text-body-secondary mb-3">
|
||||
{{ short_description|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
31
templates/single-product/stock.html.twig
Normal file
31
templates/single-product/stock.html.twig
Normal file
@@ -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 %}
|
||||
|
||||
<p class="stock {{ class|default('')|esc_attr }} mb-3">
|
||||
<span class="badge {{ badge_class }}">{{ availability|esc_html }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
<h2 class="h5 mb-3">{{ heading|esc_html }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{{ do_action('woocommerce_product_additional_information', product) }}
|
||||
26
templates/single-product/tabs/description.html.twig
Normal file
26
templates/single-product/tabs/description.html.twig
Normal file
@@ -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 %}
|
||||
<h2 class="h5 mb-3">{{ heading|esc_html }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if description is defined %}
|
||||
{{ description|raw }}
|
||||
{% else %}
|
||||
{{ the_content() }}
|
||||
{% endif %}
|
||||
56
templates/single-product/tabs/tabs.html.twig
Normal file
56
templates/single-product/tabs/tabs.html.twig
Normal file
@@ -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 %}
|
||||
<div class="woocommerce-tabs wc-tabs-wrapper mt-5">
|
||||
{# Tab navigation #}
|
||||
<ul class="nav nav-tabs" id="productTabs" role="tablist">
|
||||
{% for key, tab in product_tabs %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link{% if loop.first %} active{% endif %}"
|
||||
id="{{ key }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#tab-{{ key }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="tab-{{ key }}"
|
||||
aria-selected="{{ loop.first ? 'true' : 'false' }}">
|
||||
{{ tab.title|esc_html }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{# Tab content panels #}
|
||||
<div class="tab-content py-4" id="productTabContent">
|
||||
{% for key, tab in product_tabs %}
|
||||
<div class="tab-pane fade{% if loop.first %} show active{% endif %}"
|
||||
id="tab-{{ key }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="{{ key }}-tab"
|
||||
tabindex="0">
|
||||
{% if tab.callback is defined %}
|
||||
{{ call_user_func(tab.callback, key, tab) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_product_after_tabs') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
15
templates/single-product/title.html.twig
Normal file
15
templates/single-product/title.html.twig
Normal file
@@ -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
|
||||
#}
|
||||
|
||||
<h1 class="product_title entry-title mb-2">
|
||||
{{ the_title() }}
|
||||
</h1>
|
||||
32
templates/single-product/up-sells.html.twig
Normal file
32
templates/single-product/up-sells.html.twig
Normal file
@@ -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 %}
|
||||
<section class="up-sells upsells products mt-5">
|
||||
{% set heading = heading|default(__('You may also like…')) %}
|
||||
{% if heading %}
|
||||
<h2 class="h4 mb-4">{{ heading }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{{ woocommerce_product_loop_start() }}
|
||||
|
||||
{% for product in upsells %}
|
||||
{% include 'content-product.html.twig' with { product: product } %}
|
||||
{% endfor %}
|
||||
|
||||
{{ woocommerce_product_loop_end() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user