Implement Phase 4 & 5: cart and checkout templates (Bootstrap 5, HPOS)

Phase 4 - Cart (9 templates):
- cart: 8+4 column layout, table-responsive items, coupon input-group
- cart-empty: centered empty state with cart-x icon
- cart-item-data: inline dl for variation details
- cart-totals: card with list-group-flush rows, sticky sidebar
- cart-shipping: form-check radio per shipping method
- cross-sells: product loop grid section
- mini-cart: offcanvas-compatible item list with remove buttons
- proceed-to-checkout-button: btn-primary btn-lg w-100
- shipping-calculator: collapsible form with form-select/form-control

Phase 5 - Checkout (12 templates):
- form-checkout: 7+5 column layout, sticky order review sidebar
- form-billing: card with field wrapper, optional account creation
- form-shipping: card with ship-to-different-address collapse toggle
- form-coupon: collapsible input-group with alert-info toggle
- form-login: collapsible login reusing global/form-login.html.twig
- review-order: card with table-sm, tfoot subtotal/shipping/total
- payment: list-group of payment gateways with radio selection
- payment-method: form-check with description collapse
- terms: form-check checkbox with T&C link
- thankyou: HPOS compatible, alert-success + order details list-group
- order-received: confirmation message
- cart-errors: alert-danger with return-to-cart button

All order data accessed via WC_Order methods (HPOS compatible).
CSS additions: cart thumbnail sizing, checkout form field overrides,
payment box transitions, dark mode focus states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:33:49 +01:00
parent 9917105951
commit 594d810439
23 changed files with 1295 additions and 22 deletions

View File

@@ -0,0 +1,24 @@
{#
# Cart Errors on Checkout (Bootstrap 5 Override)
#
# Displayed when checkout cannot proceed due to cart validation errors.
#
# WooCommerce PHP equivalent: checkout/cart-errors.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{{ do_action('woocommerce_cart_has_errors') }}
<div class="alert alert-danger mb-4" role="alert">
<i class="bi bi-exclamation-triangle me-2" aria-hidden="true"></i>
{{ __('There are some issues with the items in your cart. Please go back to your cart and resolve these issues before checking out.') }}
</div>
<p>
<a href="{{ wc_get_cart_url()|esc_url }}" class="btn btn-outline-primary">
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>
{{ __('Return to cart') }}
</a>
</p>

View File

@@ -0,0 +1,66 @@
{#
# Billing Form (Bootstrap 5 Override)
#
# Renders billing address fields within a Bootstrap card.
#
# Expected context:
# checkout - WC_Checkout object with:
# .get_checkout_fields('billing') - Array of billing field configs
#
# WooCommerce PHP equivalent: checkout/form-billing.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<div class="woocommerce-billing-fields">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h3 class="h5 mb-0">{{ __('Billing details') }}</h3>
</div>
<div class="card-body">
{{ do_action('woocommerce_before_checkout_billing_form', checkout) }}
<div class="woocommerce-billing-fields__field-wrapper row g-3">
{% if billing_fields is defined %}
{% for key, field in billing_fields %}
{{ woocommerce_form_field(key, field, checkout.get_value(key)) }}
{% endfor %}
{% endif %}
</div>
{{ do_action('woocommerce_after_checkout_billing_form', checkout) }}
</div>
</div>
{# Account creation fields (for guest checkout) #}
{% if checkout.is_registration_enabled() is defined and checkout.is_registration_enabled() and not is_user_logged_in() %}
{% if checkout.is_registration_required() is not defined or not checkout.is_registration_required() %}
<div class="woocommerce-account-fields mb-4">
<div class="form-check mb-3">
<input class="form-check-input"
type="checkbox"
name="createaccount"
id="createaccount"
value="1"
{{ checkout.get_value('createaccount') ? 'checked' : '' }} />
<label class="form-check-label" for="createaccount">
{{ __('Create an account?') }}
</label>
</div>
</div>
{% endif %}
{{ do_action('woocommerce_before_checkout_registration_form', checkout) }}
<div class="create-account collapse{% if checkout.get_value('createaccount') %} show{% endif %}">
{% if account_fields is defined %}
{% for key, field in account_fields %}
{{ woocommerce_form_field(key, field, checkout.get_value(key)) }}
{% endfor %}
{% endif %}
</div>
{{ do_action('woocommerce_after_checkout_registration_form', checkout) }}
{% endif %}
</div>

View File

@@ -0,0 +1,52 @@
{#
# Checkout Form (Bootstrap 5 Override)
#
# Main checkout page: 7+5 column layout (customer details + order review).
#
# Expected context:
# checkout - WC_Checkout object
#
# WooCommerce PHP equivalent: checkout/form-checkout.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{{ do_action('woocommerce_before_checkout_form', checkout) }}
<form name="checkout" method="post" class="checkout woocommerce-checkout"
action="{{ wc_get_checkout_url()|esc_url }}" enctype="multipart/form-data">
<div class="row g-4">
{# Customer details #}
<div class="col-lg-7">
{{ do_action('woocommerce_checkout_before_customer_details') }}
<div id="customer_details">
{{ do_action('woocommerce_checkout_billing') }}
{{ do_action('woocommerce_checkout_shipping') }}
</div>
{{ do_action('woocommerce_checkout_after_customer_details') }}
</div>
{# Order review sidebar #}
<div class="col-lg-5">
<div class="position-sticky" style="top: 1rem;">
{{ do_action('woocommerce_checkout_before_order_review_heading') }}
<h3 class="h5 mb-3" id="order_review_heading">{{ __('Your order') }}</h3>
{{ do_action('woocommerce_checkout_before_order_review') }}
<div id="order_review" class="woocommerce-checkout-review-order">
{{ do_action('woocommerce_checkout_order_review') }}
</div>
{{ do_action('woocommerce_checkout_after_order_review') }}
</div>
</div>
</div>
</form>
{{ do_action('woocommerce_after_checkout_form', checkout) }}

View File

@@ -0,0 +1,45 @@
{#
# Checkout Coupon Form (Bootstrap 5 Override)
#
# Collapsible coupon code input on the checkout page.
#
# WooCommerce PHP equivalent: checkout/form-coupon.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if wc_coupons_enabled() %}
<div class="woocommerce-form-coupon-toggle mb-4">
<div class="alert alert-info d-flex align-items-center" role="status">
<i class="bi bi-tag me-2" aria-hidden="true"></i>
{{ __('Have a coupon?') }}
<a href="#checkoutCoupon"
class="ms-1 alert-link"
data-bs-toggle="collapse"
role="button"
aria-expanded="false"
aria-controls="checkoutCoupon">
{{ __('Click here to enter your code') }}
</a>
</div>
</div>
<div class="collapse mb-4" id="checkoutCoupon">
<form class="checkout_coupon woocommerce-form-coupon" method="post">
<div class="input-group">
<input type="text"
name="coupon_code"
class="form-control"
id="coupon_code_checkout"
placeholder="{{ __('Coupon code') }}" />
<button type="submit"
class="btn btn-outline-secondary"
name="apply_coupon"
value="{{ __('Apply coupon') }}">
{{ __('Apply coupon') }}
</button>
</div>
</form>
</div>
{% endif %}

View File

@@ -0,0 +1,35 @@
{#
# Checkout Login Form (Bootstrap 5 Override)
#
# Collapsible login prompt at the top of the checkout page.
#
# WooCommerce PHP equivalent: checkout/form-login.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if is_user_logged_in() is defined and not is_user_logged_in() %}
<div class="woocommerce-form-login-toggle mb-4">
<div class="alert alert-info d-flex align-items-center" role="status">
<i class="bi bi-person me-2" aria-hidden="true"></i>
{{ __('Returning customer?') }}
<a href="#checkoutLogin"
class="ms-1 alert-link"
data-bs-toggle="collapse"
role="button"
aria-expanded="false"
aria-controls="checkoutLogin">
{{ __('Click here to login') }}
</a>
</div>
</div>
<div class="collapse mb-4" id="checkoutLogin">
{% include 'global/form-login.html.twig' with {
message: __('If you have shopped with us before, please enter your details below. If you are a new customer, please proceed to the Billing section.'),
redirect: wc_get_checkout_url(),
hidden: false
} %}
</div>
{% endif %}

View File

@@ -0,0 +1,69 @@
{#
# Shipping Form (Bootstrap 5 Override)
#
# Renders shipping address fields with "ship to different address" toggle.
#
# Expected context:
# checkout - WC_Checkout object
# shipping_fields - Array of shipping field configs
# order_fields - Array of order note fields
#
# WooCommerce PHP equivalent: checkout/form-shipping.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<div class="woocommerce-shipping-fields">
<div class="card shadow-sm mb-4">
<div class="card-header">
<div class="form-check mb-0">
<input class="form-check-input"
type="checkbox"
name="ship_to_different_address"
id="ship-to-different-address-checkbox"
value="1"
{{ ship_to_different_address|default(false) ? 'checked' : '' }} />
<label class="form-check-label h5 mb-0" for="ship-to-different-address-checkbox">
{{ __('Ship to a different address?') }}
</label>
</div>
</div>
<div class="card-body shipping_address collapse{% if ship_to_different_address|default(false) %} show{% endif %}">
{{ do_action('woocommerce_before_checkout_shipping_form', checkout) }}
<div class="woocommerce-shipping-fields__field-wrapper row g-3">
{% if shipping_fields is defined %}
{% for key, field in shipping_fields %}
{{ woocommerce_form_field(key, field, checkout.get_value(key)) }}
{% endfor %}
{% endif %}
</div>
{{ do_action('woocommerce_after_checkout_shipping_form', checkout) }}
</div>
</div>
</div>
{# Order notes #}
<div class="woocommerce-additional-fields mb-4">
{{ do_action('woocommerce_before_order_notes', checkout) }}
{% if order_fields is defined and order_fields|length > 0 %}
<div class="card shadow-sm">
<div class="card-header">
<h3 class="h5 mb-0">{{ __('Additional information') }}</h3>
</div>
<div class="card-body">
<div class="woocommerce-additional-fields__field-wrapper">
{% for key, field in order_fields %}
{{ woocommerce_form_field(key, field, checkout.get_value(key)) }}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{{ do_action('woocommerce_after_order_notes', checkout) }}
</div>

View File

@@ -0,0 +1,18 @@
{#
# Order Received Message (Bootstrap 5 Override)
#
# Short confirmation text displayed on the thank-you page.
# HPOS compatible: no $post global, uses WC_Order methods only.
#
# Expected context:
# order - WC_Order object (optional)
#
# WooCommerce PHP equivalent: checkout/order-received.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<p class="woocommerce-notice woocommerce-notice--success woocommerce-thankyou-order-received mb-0">
{{ __('Thank you. Your order has been received.') }}
</p>

View File

@@ -0,0 +1,46 @@
{#
# Single Payment Method (Bootstrap 5 Override)
#
# Renders a payment gateway as a list-group-item with radio and description.
#
# Expected context:
# gateway - WC_Payment_Gateway object with:
# .id - Gateway ID
# .get_title() - Gateway title
# .get_icon() - Gateway icon HTML
# .has_fields() - Whether gateway has inline fields
# .get_description() - Gateway description
# .chosen - Whether this is the selected gateway
# .payment_fields() - Renders inline payment fields
#
# WooCommerce PHP equivalent: checkout/payment-method.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<li class="wc_payment_method payment_method_{{ gateway.id|esc_attr }} list-group-item">
<div class="form-check">
<input type="radio"
class="form-check-input input-radio"
name="payment_method"
id="payment_method_{{ gateway.id|esc_attr }}"
value="{{ gateway.id|esc_attr }}"
data-order_button_text="{{ gateway.order_button_text|default('')|esc_attr }}"
{% if gateway.chosen is defined and gateway.chosen %}checked{% endif %} />
<label class="form-check-label d-flex align-items-center gap-2" for="payment_method_{{ gateway.id|esc_attr }}">
<span>{{ gateway.get_title()|esc_html }}</span>
{{ gateway.get_icon()|raw }}
</label>
</div>
{% if gateway.has_fields() or gateway.get_description() %}
<div class="payment_box payment_method_{{ gateway.id|esc_attr }} mt-2 ps-4{% if not gateway.chosen|default(false) %} d-none{% endif %}">
{% if gateway.get_description() %}
<p class="small text-body-secondary mb-2">{{ gateway.get_description()|wp_kses_post }}</p>
{% endif %}
{{ gateway.payment_fields() }}
</div>
{% endif %}
</li>

View File

@@ -0,0 +1,54 @@
{#
# Payment Methods & Place Order (Bootstrap 5 Override)
#
# Renders payment gateway selection and the place order button.
#
# Expected context:
# available_gateways - Array of WC_Payment_Gateway objects
# order_button_text - Place order button label
#
# WooCommerce PHP equivalent: checkout/payment.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<div id="payment" class="woocommerce-checkout-payment mt-4">
{{ do_action('woocommerce_review_order_before_payment') }}
{% if available_gateways is defined and available_gateways|length > 0 %}
<div class="card shadow-sm mb-3">
<div class="card-header">
<h4 class="h6 mb-0">{{ __('Payment method') }}</h4>
</div>
<ul class="wc_payment_methods payment_methods list-group list-group-flush">
{% for gateway_id, gateway in available_gateways %}
{% include 'checkout/payment-method.html.twig' with { gateway: gateway } %}
{% endfor %}
</ul>
</div>
{% else %}
<div class="alert alert-warning mb-3" role="alert">
{{ __('Sorry, it seems that there are no available payment methods. Please contact us if you require assistance or wish to make alternate arrangements.') }}
</div>
{% endif %}
<div class="place-order">
{{ do_action('woocommerce_review_order_before_submit') }}
{{ wp_nonce_field('woocommerce-process_checkout', 'woocommerce-process-checkout-nonce') }}
<button type="submit"
class="btn btn-primary btn-lg w-100"
name="woocommerce_checkout_place_order"
id="place_order"
value="{{ order_button_text|default(__('Place order')) }}"
data-value="{{ order_button_text|default(__('Place order')) }}">
{{ order_button_text|default(__('Place order')) }}
</button>
{{ do_action('woocommerce_review_order_after_submit') }}
</div>
{{ do_action('woocommerce_review_order_after_payment') }}
</div>

View File

@@ -0,0 +1,91 @@
{#
# Order Review Table (Bootstrap 5 Override)
#
# Renders the order summary table during checkout.
#
# Expected context:
# cart_items - Array of cart items for review
# cart_subtotal - Subtotal HTML
# cart_total - Total HTML
# coupons - Applied coupons
# fees - Fees
#
# WooCommerce PHP equivalent: checkout/review-order.php
#
# @package WcBootstrap
# @since 0.1.0
#}
<div class="card shadow-sm woocommerce-checkout-review-order-table">
<div class="card-body p-0">
<table class="table table-sm mb-0 shop_table woocommerce-checkout-review-order-table">
<thead class="table-light">
<tr>
<th class="product-name" scope="col">{{ __('Product') }}</th>
<th class="product-total text-end" scope="col">{{ __('Subtotal') }}</th>
</tr>
</thead>
<tbody>
{{ do_action('woocommerce_review_order_before_cart_contents') }}
{% if cart_items is defined %}
{% for item in cart_items %}
<tr class="cart_item">
<td class="product-name">
{{ item.product_name|esc_html }}
<strong class="product-quantity text-body-secondary">&times;&nbsp;{{ item.quantity }}</strong>
{{ item.item_data_html|default('')|raw }}
</td>
<td class="product-total text-end">
{{ item.subtotal|raw }}
</td>
</tr>
{% endfor %}
{% endif %}
{{ do_action('woocommerce_review_order_after_cart_contents') }}
</tbody>
<tfoot>
<tr class="cart-subtotal">
<th scope="row">{{ __('Subtotal') }}</th>
<td class="text-end">{{ cart_subtotal|raw }}</td>
</tr>
{% if coupons is defined %}
{% for coupon in coupons %}
<tr class="cart-discount coupon-{{ coupon.code|esc_attr }}">
<th scope="row">{{ __('Coupon:') }} {{ coupon.code|esc_html }}</th>
<td class="text-end">{{ coupon.discount_html|raw }}</td>
</tr>
{% endfor %}
{% endif %}
{{ do_action('woocommerce_review_order_before_shipping') }}
{% if wc_shipping_enabled() is defined and wc_shipping_enabled() %}
{{ do_action('woocommerce_review_order_shipping') }}
{% endif %}
{{ do_action('woocommerce_review_order_after_shipping') }}
{% if fees is defined %}
{% for fee in fees %}
<tr class="fee">
<th scope="row">{{ fee.name|esc_html }}</th>
<td class="text-end">{{ fee.total_html|raw }}</td>
</tr>
{% endfor %}
{% endif %}
{{ do_action('woocommerce_review_order_before_order_total') }}
<tr class="order-total">
<th scope="row" class="fw-bold">{{ __('Total') }}</th>
<td class="text-end fw-bold fs-5">{{ cart_total|raw }}</td>
</tr>
{{ do_action('woocommerce_review_order_after_order_total') }}
</tfoot>
</table>
</div>
</div>

View File

@@ -0,0 +1,32 @@
{#
# Terms & Conditions Checkbox (Bootstrap 5 Override)
#
# WooCommerce PHP equivalent: checkout/terms.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{{ do_action('woocommerce_checkout_before_terms_and_conditions') }}
{% if wc_terms_and_conditions_checkbox_enabled() is defined and wc_terms_and_conditions_checkbox_enabled() %}
<div class="woocommerce-terms-and-conditions-wrapper mb-3">
{{ do_action('woocommerce_checkout_terms_and_conditions') }}
<div class="form-check">
<input type="checkbox"
class="form-check-input woocommerce-form__input-checkbox"
name="terms"
id="terms"
{% if terms_checked is defined and terms_checked %}checked{% endif %}
required
aria-required="true" />
<label class="form-check-label woocommerce-form__label-for-checkbox" for="terms">
{{ wc_terms_and_conditions_checkbox_text()|raw }}
</label>
<input type="hidden" name="terms-field" value="1" />
</div>
</div>
{% endif %}
{{ do_action('woocommerce_checkout_after_terms_and_conditions') }}

View File

@@ -0,0 +1,75 @@
{#
# Thank You / Order Confirmation (Bootstrap 5 Override)
#
# HPOS compatible: uses WC_Order object methods only, no $post global.
#
# Expected context:
# order - WC_Order object (or null on failure)
#
# WooCommerce PHP equivalent: checkout/thankyou.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{{ do_action('woocommerce_before_thankyou') }}
{% if order is defined and order %}
{% if order.has_status('failed') %}
<div class="alert alert-danger mb-4" role="alert">
<p class="mb-0">
{{ __('Unfortunately your order cannot be processed as the originating bank/merchant has declined your transaction. Please attempt your purchase again.') }}
</p>
</div>
<p>
<a href="{{ order.get_checkout_payment_url()|esc_url }}" class="btn btn-outline-primary">
{{ __('Pay') }}
</a>
</p>
{% else %}
<div class="alert alert-success mb-4" role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{% include 'checkout/order-received.html.twig' with { order: order } %}
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">
<h2 class="h5 mb-0">{{ __('Order details') }}</h2>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span class="text-body-secondary">{{ __('Order number:') }}</span>
<strong>{{ order.get_order_number() }}</strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span class="text-body-secondary">{{ __('Date:') }}</span>
<strong>{{ order.get_date_created().date_i18n(wc_date_format()) }}</strong>
</li>
{% if order.get_billing_email() %}
<li class="list-group-item d-flex justify-content-between">
<span class="text-body-secondary">{{ __('Email:') }}</span>
<strong>{{ order.get_billing_email()|esc_html }}</strong>
</li>
{% endif %}
<li class="list-group-item d-flex justify-content-between">
<span class="text-body-secondary">{{ __('Total:') }}</span>
<strong>{{ order.get_formatted_order_total()|raw }}</strong>
</li>
{% if order.get_payment_method_title() %}
<li class="list-group-item d-flex justify-content-between">
<span class="text-body-secondary">{{ __('Payment method:') }}</span>
<strong>{{ order.get_payment_method_title()|esc_html }}</strong>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{{ do_action('woocommerce_thankyou_' ~ order.get_payment_method(), order.get_id()) }}
{{ do_action('woocommerce_thankyou', order.get_id()) }}
{% else %}
<div class="alert alert-success mb-4" role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{% include 'checkout/order-received.html.twig' %}
</div>
{% endif %}