From c9c99a6b885fd57718ad77b59d1edc5abcd8d9f7 Mon Sep 17 00:00:00 2001 From: magdev Date: Sat, 28 Feb 2026 10:23:09 +0100 Subject: [PATCH] Implement Phase 2: product archive and shop loop templates (Bootstrap 5) Add 15 Twig template overrides for the product archive and shop loop: - archive-product: 3+9 grid layout with optional filter sidebar - content-product: card component with hook-based content injection - content-product-cat: category card with thumbnail - product-searchform: input-group with search icon button - loop/loop-start, loop-end: responsive row-cols grid - loop/header: archive title with description hook - loop/result-count: showing X-Y of Z with aria-relevant - loop/orderby: form-select-sm sort dropdown - loop/pagination: delegates to components/pagination.html.twig - loop/no-products-found: alert-info empty state - loop/add-to-cart: btn-primary-sm with AJAX data attributes - loop/price: fw-semibold with sale/regular markup - loop/rating: Bootstrap Icon stars (full, half, empty) - loop/sale-flash: badge bg-danger positioned overlay CSS additions: product card hover, sale badge z-index, star rating sizing, price del/ins styling, WooCommerce grid reset. Co-Authored-By: Claude Opus 4.6 --- PLAN.md | 30 +++++----- assets/css/wc-bootstrap.css | 65 ++++++++++++++++++++++ templates/archive-product.html.twig | 64 +++++++++++++++++++++ templates/content-product-cat.html.twig | 34 +++++++++++ templates/content-product.html.twig | 44 +++++++++++++++ templates/loop/add-to-cart.html.twig | 47 ++++++++++++++++ templates/loop/header.html.twig | 25 +++++++++ templates/loop/loop-end.html.twig | 12 ++++ templates/loop/loop-start.html.twig | 17 ++++++ templates/loop/no-products-found.html.twig | 17 ++++++ templates/loop/orderby.html.twig | 40 +++++++++++++ templates/loop/pagination.html.twig | 24 ++++++++ templates/loop/price.html.twig | 21 +++++++ templates/loop/rating.html.twig | 40 +++++++++++++ templates/loop/result-count.html.twig | 33 +++++++++++ templates/loop/sale-flash.html.twig | 21 +++++++ templates/product-searchform.html.twig | 32 +++++++++++ 17 files changed, 551 insertions(+), 15 deletions(-) create mode 100644 templates/archive-product.html.twig create mode 100644 templates/content-product-cat.html.twig create mode 100644 templates/content-product.html.twig create mode 100644 templates/loop/add-to-cart.html.twig create mode 100644 templates/loop/header.html.twig create mode 100644 templates/loop/loop-end.html.twig create mode 100644 templates/loop/loop-start.html.twig create mode 100644 templates/loop/no-products-found.html.twig create mode 100644 templates/loop/orderby.html.twig create mode 100644 templates/loop/pagination.html.twig create mode 100644 templates/loop/price.html.twig create mode 100644 templates/loop/rating.html.twig create mode 100644 templates/loop/result-count.html.twig create mode 100644 templates/loop/sale-flash.html.twig create mode 100644 templates/product-searchform.html.twig diff --git a/PLAN.md b/PLAN.md index e1f3bbe..5b26627 100644 --- a/PLAN.md +++ b/PLAN.md @@ -531,21 +531,21 @@ Track completion per file. Mark with `[x]` when done. ### Phase 2 -- Archive & Loop -- [ ] `archive-product.html.twig` -- [ ] `content-product.html.twig` -- [ ] `content-product-cat.html.twig` -- [ ] `product-searchform.html.twig` -- [ ] `loop/loop-start.html.twig` -- [ ] `loop/loop-end.html.twig` -- [ ] `loop/header.html.twig` -- [ ] `loop/result-count.html.twig` -- [ ] `loop/orderby.html.twig` -- [ ] `loop/pagination.html.twig` -- [ ] `loop/no-products-found.html.twig` -- [ ] `loop/add-to-cart.html.twig` -- [ ] `loop/price.html.twig` -- [ ] `loop/rating.html.twig` -- [ ] `loop/sale-flash.html.twig` +- [x] `archive-product.html.twig` +- [x] `content-product.html.twig` +- [x] `content-product-cat.html.twig` +- [x] `product-searchform.html.twig` +- [x] `loop/loop-start.html.twig` +- [x] `loop/loop-end.html.twig` +- [x] `loop/header.html.twig` +- [x] `loop/result-count.html.twig` +- [x] `loop/orderby.html.twig` +- [x] `loop/pagination.html.twig` +- [x] `loop/no-products-found.html.twig` +- [x] `loop/add-to-cart.html.twig` +- [x] `loop/price.html.twig` +- [x] `loop/rating.html.twig` +- [x] `loop/sale-flash.html.twig` ### Phase 3 -- Single Product diff --git a/assets/css/wc-bootstrap.css b/assets/css/wc-bootstrap.css index c212373..ee33114 100644 --- a/assets/css/wc-bootstrap.css +++ b/assets/css/wc-bootstrap.css @@ -88,6 +88,71 @@ margin: 0; } +/* ========================================================================== + Product Cards + Hover effects and layout for product loop cards. + ========================================================================== */ + +.product.card { + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.product.card:hover { + transform: translateY(-2px); + box-shadow: var(--bs-box-shadow) !important; +} + +/* Product image in card */ +.product.card img { + object-fit: cover; + aspect-ratio: 1 / 1; + width: 100%; +} + +/* ========================================================================== + Sale Badge + Positioning for the sale overlay badge on product cards. + ========================================================================== */ + +.onsale { + z-index: 1; +} + +/* ========================================================================== + Star Rating + Consistent sizing for Bootstrap Icon star ratings. + ========================================================================== */ + +.wc-star-rating .bi { + font-size: 0.875rem; +} + +/* ========================================================================== + Price Styling + Override WooCommerce default price markup with Bootstrap-aligned styles. + ========================================================================== */ + +.price del { + text-decoration: line-through; + color: var(--bs-secondary-color); + font-weight: 400; +} + +.price ins { + text-decoration: none; + color: var(--bs-danger); + font-weight: 600; +} + +/* ========================================================================== + WooCommerce Grid Override + Reset WooCommerce's default grid to let Bootstrap handle layout. + ========================================================================== */ + +.woocommerce ul.products { + display: contents; +} + /* ========================================================================== Dark Mode Overrides Fix any plugin elements that don't adapt to Bootstrap's dark mode. diff --git a/templates/archive-product.html.twig b/templates/archive-product.html.twig new file mode 100644 index 0000000..b91d85d --- /dev/null +++ b/templates/archive-product.html.twig @@ -0,0 +1,64 @@ +{# + # Product Archive / Shop Page (Bootstrap 5 Override) + # + # Main template for the shop page, product category, tag, and attribute archives. + # Uses a 3+9 column layout with a filter sidebar and product grid. + # + # Expected context: + # page_title - Archive page title + # has_products - Whether the loop has products + # products - Array of product objects for the loop + # sidebar_content - Pre-rendered sidebar HTML + # + # WooCommerce PHP equivalent: archive-product.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% extends "base.html.twig" %} + +{% block breadcrumbs %} + {{ do_action('woocommerce_before_main_content') }} +{% endblock %} + +{% block content %} + {{ do_action('woocommerce_shop_loop_header') }} + + {% if has_products is defined and has_products %} + {{ do_action('woocommerce_before_shop_loop') }} + +
+ {# Sidebar with filters #} + {% if sidebar_content is defined and sidebar_content %} + +
+ {% else %} +
+ {% endif %} + + {{ woocommerce_product_loop_start() }} + + {% if products is defined %} + {% for product in products %} + {% include 'content-product.html.twig' with { product: product } %} + {% endfor %} + {% endif %} + + {{ woocommerce_product_loop_end() }} + + {{ do_action('woocommerce_after_shop_loop') }} +
+
+ {% else %} + {{ do_action('woocommerce_no_products_found') }} + {% endif %} + + {{ do_action('woocommerce_after_main_content') }} +{% endblock %} + +{% block sidebar %} + {{ do_action('woocommerce_sidebar') }} +{% endblock %} diff --git a/templates/content-product-cat.html.twig b/templates/content-product-cat.html.twig new file mode 100644 index 0000000..9a8d3de --- /dev/null +++ b/templates/content-product-cat.html.twig @@ -0,0 +1,34 @@ +{# + # Product Category Content in Loop (Bootstrap 5 Override) + # + # Renders a product category card within the shop loop grid. + # + # Expected context: + # category - Product category object with: + # .name - Category name + # .count - Number of products + # .link - Category URL + # .thumbnail - Category thumbnail HTML + # + # WooCommerce PHP equivalent: content-product-cat.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+
+ {{ do_action('woocommerce_before_subcategory', category) }} + +
+ {{ do_action('woocommerce_before_subcategory_title', category) }} +
+ +
+ {{ do_action('woocommerce_shop_loop_subcategory_title', category) }} + {{ do_action('woocommerce_after_subcategory_title', category) }} +
+ + {{ do_action('woocommerce_after_subcategory', category) }} +
+
diff --git a/templates/content-product.html.twig b/templates/content-product.html.twig new file mode 100644 index 0000000..9075991 --- /dev/null +++ b/templates/content-product.html.twig @@ -0,0 +1,44 @@ +{# + # Product Content in Loop (Bootstrap 5 Override) + # + # Renders a single product card within the shop loop grid. + # Uses Bootstrap 5 card component with stretched-link. + # + # Expected context: + # product - WC_Product object with: + # .name - Product name + # .permalink - Product URL + # .image - Product thumbnail HTML + # .price_html - Formatted price HTML + # .rating_html - Star rating HTML + # .is_on_sale - Whether product is on sale + # .add_to_cart - Add-to-cart button context + # + # WooCommerce PHP equivalent: content-product.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+
+ {{ do_action('woocommerce_before_shop_loop_item') }} + + {# Product image with sale badge overlay #} +
+ {{ do_action('woocommerce_before_shop_loop_item_title') }} +
+ +
+ {# Product title #} + {{ do_action('woocommerce_shop_loop_item_title') }} + + {# Rating and price #} + {{ do_action('woocommerce_after_shop_loop_item_title') }} +
+ + +
+
diff --git a/templates/loop/add-to-cart.html.twig b/templates/loop/add-to-cart.html.twig new file mode 100644 index 0000000..af2adcc --- /dev/null +++ b/templates/loop/add-to-cart.html.twig @@ -0,0 +1,47 @@ +{# + # Loop Add to Cart Button (Bootstrap 5 Override) + # + # Renders the add-to-cart button within the product loop. + # + # Expected context: + # product - WC_Product object with: + # .add_to_cart_url() - Add to cart URL + # .add_to_cart_text() - Button label + # .is_purchasable() - Whether product can be purchased + # .is_in_stock() - Whether product is in stock + # .supports('ajax_add_to_cart') - Whether AJAX add to cart is supported + # .get_id() - Product ID + # args - Array with: + # .quantity - Quantity (default: 1) + # .class - CSS classes + # .attributes - Additional HTML attributes + # .aria-describedby_text - Accessibility description + # + # WooCommerce PHP equivalent: loop/add-to-cart.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set quantity = args.quantity|default(1) %} +{% set btn_class = 'btn btn-primary btn-sm w-100' %} + + + {{ product.add_to_cart_text() }} + + +{% if args['aria-describedby_text'] is defined and args['aria-describedby_text'] %} + + {{ args['aria-describedby_text'] }} + +{% endif %} diff --git a/templates/loop/header.html.twig b/templates/loop/header.html.twig new file mode 100644 index 0000000..cb96a85 --- /dev/null +++ b/templates/loop/header.html.twig @@ -0,0 +1,25 @@ +{# + # Product Archive Header (Bootstrap 5 Override) + # + # Renders the archive page title and optional description. + # + # Expected context: + # show_page_title - Whether to display the title (boolean, filtered) + # page_title - Archive page title string + # archive_description - Optional archive description HTML + # + # WooCommerce PHP equivalent: loop/header.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+ {% if show_page_title is not defined or show_page_title %} +

+ {{ page_title|default('')|esc_html }} +

+ {% endif %} + + {{ do_action('woocommerce_archive_description') }} +
diff --git a/templates/loop/loop-end.html.twig b/templates/loop/loop-end.html.twig new file mode 100644 index 0000000..bd24230 --- /dev/null +++ b/templates/loop/loop-end.html.twig @@ -0,0 +1,12 @@ +{# + # Product Loop End (Bootstrap 5 Override) + # + # Closes the product grid container opened by loop-start.html.twig. + # + # WooCommerce PHP equivalent: loop/loop-end.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
diff --git a/templates/loop/loop-start.html.twig b/templates/loop/loop-start.html.twig new file mode 100644 index 0000000..b89b25d --- /dev/null +++ b/templates/loop/loop-start.html.twig @@ -0,0 +1,17 @@ +{# + # Product Loop Start (Bootstrap 5 Override) + # + # Opens the product grid container using Bootstrap 5 row with responsive columns. + # + # Expected context: + # columns - Number of columns (from wc_get_loop_prop) + # + # WooCommerce PHP equivalent: loop/loop-start.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set cols = columns|default(3) %} + +
diff --git a/templates/loop/no-products-found.html.twig b/templates/loop/no-products-found.html.twig new file mode 100644 index 0000000..bc52b9c --- /dev/null +++ b/templates/loop/no-products-found.html.twig @@ -0,0 +1,17 @@ +{# + # No Products Found (Bootstrap 5 Override) + # + # Displayed when the product archive has no results. + # + # WooCommerce PHP equivalent: loop/no-products-found.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+ +
diff --git a/templates/loop/orderby.html.twig b/templates/loop/orderby.html.twig new file mode 100644 index 0000000..27485aa --- /dev/null +++ b/templates/loop/orderby.html.twig @@ -0,0 +1,40 @@ +{# + # Catalog Ordering / Sort Dropdown (Bootstrap 5 Override) + # + # Renders the product sort-by dropdown as a Bootstrap 5 form-select. + # + # Expected context: + # catalog_orderby_options - Associative array of { value: label } sort options + # orderby - Currently selected orderby value + # use_label - Whether to display a label (boolean) + # + # WooCommerce PHP equivalent: loop/orderby.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +
+ {% if use_label is defined and use_label %} + + {% endif %} + + + + + {{ wc_query_string_form_fields() }} +
diff --git a/templates/loop/pagination.html.twig b/templates/loop/pagination.html.twig new file mode 100644 index 0000000..0365293 --- /dev/null +++ b/templates/loop/pagination.html.twig @@ -0,0 +1,24 @@ +{# + # Product Pagination (Bootstrap 5 Override) + # + # Renders pagination for the product archive using Bootstrap 5 pagination component. + # + # Expected context: + # total - Total number of pages + # current - Current page number (1-based) + # + # WooCommerce PHP equivalent: loop/pagination.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set max_pages = total|default(1) %} +{% set current_page = current|default(1) %} + +{% if max_pages > 1 %} + {% include 'components/pagination.html.twig' with { + current_page: current_page, + max_pages: max_pages + } %} +{% endif %} diff --git a/templates/loop/price.html.twig b/templates/loop/price.html.twig new file mode 100644 index 0000000..0f1433b --- /dev/null +++ b/templates/loop/price.html.twig @@ -0,0 +1,21 @@ +{# + # Loop Product Price (Bootstrap 5 Override) + # + # Renders the product price within the shop loop. + # Price HTML is pre-formatted by WooCommerce (includes sale/regular markup). + # + # Expected context: + # product - WC_Product object with: + # .get_price_html() - Formatted price HTML (includes / for sales) + # + # WooCommerce PHP equivalent: loop/price.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product is defined %} + + {{ product.get_price_html()|raw }} + +{% endif %} diff --git a/templates/loop/rating.html.twig b/templates/loop/rating.html.twig new file mode 100644 index 0000000..3458ceb --- /dev/null +++ b/templates/loop/rating.html.twig @@ -0,0 +1,40 @@ +{# + # Loop Product Rating (Bootstrap 5 Override) + # + # Renders star ratings for products in the shop loop. + # + # Expected context: + # product - WC_Product object with: + # .get_average_rating() - Average rating (0-5) + # .get_review_count() - Number of reviews + # reviews_enabled - Whether reviews/ratings are enabled (boolean) + # + # WooCommerce PHP equivalent: loop/rating.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if reviews_enabled is not defined or reviews_enabled %} + {% if product is defined and product.get_average_rating() > 0 %} + {% set rating = product.get_average_rating() %} + {% set count = product.get_review_count() %} + + + {% endif %} +{% endif %} diff --git a/templates/loop/result-count.html.twig b/templates/loop/result-count.html.twig new file mode 100644 index 0000000..9830183 --- /dev/null +++ b/templates/loop/result-count.html.twig @@ -0,0 +1,33 @@ +{# + # Result Count (Bootstrap 5 Override) + # + # Displays the "Showing X-Y of Z results" text. + # + # Expected context: + # total - Total number of products + # per_page - Products per page + # current - Current page number + # orderedby - Whether results are currently sorted (optional) + # + # WooCommerce PHP equivalent: loop/result-count.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + + diff --git a/templates/loop/sale-flash.html.twig b/templates/loop/sale-flash.html.twig new file mode 100644 index 0000000..42375a2 --- /dev/null +++ b/templates/loop/sale-flash.html.twig @@ -0,0 +1,21 @@ +{# + # Sale Badge (Bootstrap 5 Override) + # + # Renders a sale badge overlay on product cards. + # + # Expected context: + # product - WC_Product object with: + # .is_on_sale() - Whether product is currently on sale + # post - Global post object + # + # WooCommerce PHP equivalent: loop/sale-flash.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% if product is defined and product.is_on_sale() %} + + {{ __('Sale!') }} + +{% endif %} diff --git a/templates/product-searchform.html.twig b/templates/product-searchform.html.twig new file mode 100644 index 0000000..a08df19 --- /dev/null +++ b/templates/product-searchform.html.twig @@ -0,0 +1,32 @@ +{# + # Product Search Form (Bootstrap 5 Override) + # + # Renders the WooCommerce product search form as a Bootstrap 5 input-group. + # + # Expected context: + # index - Unique form index (for multiple search forms on a page) + # + # WooCommerce PHP equivalent: product-searchform.php + # + # @package WcBootstrap + # @since 0.1.0 + #} + +{% set field_id = 'woocommerce-product-search-field-' ~ (index|default(0)) %} + +