You've already forked wc-bootstrap
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01404c87ba | |||
| f3e7ede60c | |||
| 6ee95f4a2f | |||
| c5f8e88ee4 | |||
| 7fda8e1962 | |||
| 00872a6568 | |||
| 7034134678 | |||
| b8001a5ab0 | |||
| c3b16b68c5 | |||
| c0d1dc85c4 | |||
| 624de0cae6 |
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git *)",
|
||||||
|
"Bash(docker exec woocommerce *)",
|
||||||
|
"Bash(docker compose *)",
|
||||||
|
"Bash(composer *)",
|
||||||
|
"Bash(npm *)",
|
||||||
|
"Bash(ls *)",
|
||||||
|
"Bash(mkdir *)",
|
||||||
|
"Bash(cat *)",
|
||||||
|
"Bash(php *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
|
.claude
|
||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
vendor
|
||||||
|
releases
|
||||||
|
wp-core
|
||||||
|
wc-core
|
||||||
|
CLAUDE.md
|
||||||
|
PLAN.md
|
||||||
|
SETUP.md
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
|
*.po~
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
19
.env-dist
Normal file
19
.env-dist
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Database
|
||||||
|
MYSQL_ROOT_PASSWORD=rootpass
|
||||||
|
MYSQL_DATABASE=wordpress
|
||||||
|
MYSQL_USER=wordpress
|
||||||
|
MYSQL_PASSWORD=wordpress
|
||||||
|
|
||||||
|
# WordPress
|
||||||
|
WP_PORT=8080
|
||||||
|
|
||||||
|
# Auto-setup (runs on first boot, set to 0 to disable)
|
||||||
|
WP_AUTO_SETUP=1
|
||||||
|
WP_URL=http://localhost:8080
|
||||||
|
WP_TITLE=WC Bootstrap Dev
|
||||||
|
WP_ADMIN_USER=admin
|
||||||
|
WP_ADMIN_PASSWORD=admin
|
||||||
|
WP_ADMIN_EMAIL=admin@example.com
|
||||||
|
|
||||||
|
# Build args (used during `docker compose build`)
|
||||||
|
# WOOCOMMERCE_VERSION=latest
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,3 +31,6 @@ npm-debug.log
|
|||||||
|
|
||||||
# Build artifacts (releases directory)
|
# Build artifacts (releases directory)
|
||||||
releases/
|
releases/
|
||||||
|
|
||||||
|
# Docker runtime
|
||||||
|
.env
|
||||||
|
|||||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -2,6 +2,37 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-02-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Docker development environment: multistage Dockerfile (WooCommerce download, wp-bootstrap npm build, Composer deps, WordPress runtime), Compose stack with MariaDB, auto-setup entrypoint
|
||||||
|
- Private registry image name and restart policies for Docker stack
|
||||||
|
- **My Account polish**: endpoint icon map, `offcanvas-lg` responsive navigation with sticky sidebar, card-based dashboard with avatar welcome greeting and quick-action grid, card-wrapped forms with icon headers, view-order summary card with status badge
|
||||||
|
- **Product archive**: Bootstrap 5 card grid with responsive columns, sale badges, star ratings, offcanvas sidebar for mobile filters, shop-sidebar widget area
|
||||||
|
- **Single product layout**: two-column responsive grid (image gallery + product summary), bridge for `wc_get_template_part()` interception, disabled WooCommerce block compatibility layer
|
||||||
|
- Global `$product` injection into Twig context for loop sub-templates
|
||||||
|
- CSS overrides: WooCommerce float/width layout reset, sale badge repositioning, shop_table border reset, gallery opacity fallback, My Account `max-width` reset
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Rename `base.html.twig` → `wc-base.html.twig` to prevent shadowing the parent theme's page shell via Twig `FilesystemLoader` path priority
|
||||||
|
- CSS dependency handle from unregistered `woocommerce` to `woocommerce-general` for `wc-bootstrap-overrides` stylesheet
|
||||||
|
- WooCommerce float-based two-column layout (`width: 48%` + `float`) fighting Bootstrap grid on single product pages
|
||||||
|
- Nested content wrapper doubling parent theme's `.container`
|
||||||
|
- Sale badge escaping image column and blocking breadcrumb clicks
|
||||||
|
- `|nl2br|esc_html` filter order in order details (was escaping `<br>` tags)
|
||||||
|
- HTML entity double-encoding (`…`) in up-sells, cross-sells, and related product headings
|
||||||
|
- Wrong `function() is defined` guards in cart totals, review order, checkout login, and terms templates
|
||||||
|
- Duplicate deprecated hook fires in dashboard template
|
||||||
|
- Missing `|raw` on brand description HTML filter chain
|
||||||
|
- Missing `role="alert"` on variation add-to-cart for accessibility
|
||||||
|
- Missing `|esc_attr` on notification type class attribute
|
||||||
|
- Missing `is defined` guard on `hidden` variable in global form-login
|
||||||
|
- Pagination URLs using `?page=` instead of `get_pagenum_link()` for archives
|
||||||
|
- Double-escaped `–` in result count
|
||||||
|
- Underscore.js triple-brace syntax conflict in variation template (wrapped in `{% verbatim %}`)
|
||||||
|
|
||||||
## [0.0.1] - 2026-02-28
|
## [0.0.1] - 2026-02-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -27,6 +58,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Theme wrapping signal via `woocommerce_is_theme_wrapped` filter
|
- Theme wrapping signal via `woocommerce_is_theme_wrapped` filter
|
||||||
- CI/CD release workflow (Gitea Actions)
|
- CI/CD release workflow (Gitea Actions)
|
||||||
- Translation support (`.pot` template ready)
|
- Translation support (`.pot` template ready)
|
||||||
|
- Docker development environment (multistage Dockerfile, Compose, auto-setup entrypoint)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Rename `base.html.twig` → `wc-base.html.twig` to prevent shadowing the parent theme's page shell via Twig `FilesystemLoader` path priority
|
||||||
|
- Correct CSS dependency handle from `woocommerce` to `woocommerce-general` for `wc-bootstrap-overrides` stylesheet
|
||||||
|
|
||||||
### Skipped
|
### Skipped
|
||||||
|
|
||||||
|
|||||||
81
CLAUDE.md
81
CLAUDE.md
@@ -215,6 +215,10 @@ Recurring bugs and non-obvious behaviours discovered across sessions. **Read thi
|
|||||||
- **Twig autoescape + WordPress escape filters = double encoding** -- register all `esc_*` filters with `['is_safe' => ['html']]` option in the plugin's `Template.php`.
|
- **Twig autoescape + WordPress escape filters = double encoding** -- register all `esc_*` filters with `['is_safe' => ['html']]` option in the plugin's `Template.php`.
|
||||||
- **`wp i18n make-pot` does NOT scan Twig templates** -- any string used exclusively in `.html.twig` files must be manually added to the `.pot` file.
|
- **`wp i18n make-pot` does NOT scan Twig templates** -- any string used exclusively in `.html.twig` files must be manually added to the `.pot` file.
|
||||||
- **`#, fuzzy` silently skips translations at runtime** -- always remove fuzzy flags after verifying translations.
|
- **`#, fuzzy` silently skips translations at runtime** -- always remove fuzzy flags after verifying translations.
|
||||||
|
- **`|nl2br|esc_html` is wrong filter order** -- `nl2br` outputs `<br>` tags, then `esc_html` escapes them to `<br>`. Correct: `|esc_html|nl2br`.
|
||||||
|
- **`function() is defined` is semantically wrong** -- always evaluates truthy since the function is called regardless. Use `{% if function() %}` directly.
|
||||||
|
- **HTML entities in translated strings get double-encoded** -- `…` in `__()` becomes `&hellip;`. Use Unicode `…` directly or append `|raw` for trusted filter output.
|
||||||
|
- **Filter chains producing HTML need `|raw`** -- e.g., `term_description()|wptexturize|wpautop|do_shortcode|raw`.
|
||||||
|
|
||||||
### Bootstrap 5 vs Plugin CSS Conflicts
|
### Bootstrap 5 vs Plugin CSS Conflicts
|
||||||
|
|
||||||
@@ -222,6 +226,10 @@ Recurring bugs and non-obvious behaviours discovered across sessions. **Read thi
|
|||||||
- **CSS dependency chain**: `woocommerce` -> child theme overrides. Ensures correct cascade.
|
- **CSS dependency chain**: `woocommerce` -> child theme overrides. Ensures correct cascade.
|
||||||
- jQuery `.show()`/`.hide()` cannot override Bootstrap `!important` (`d-none`). Toggle both class and inline style.
|
- jQuery `.show()`/`.hide()` cannot override Bootstrap `!important` (`d-none`). Toggle both class and inline style.
|
||||||
- `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation.
|
- `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation.
|
||||||
|
- **WooCommerce `shop_table` borders conflict with Bootstrap `.table`** -- reset with `.woocommerce table.shop_table { border: 0 }` and cell `border-left/right: 0`.
|
||||||
|
- **WooCommerce gallery JS requires modifier classes** -- `--with-images`, `--without-images`, `--columns-N` on `.woocommerce-product-gallery` + `style="opacity: 0"` for fade-in. Without these, zoom and photoswipe won't initialize.
|
||||||
|
- **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`.
|
||||||
|
- **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired.
|
||||||
|
|
||||||
### Double Heading Prevention
|
### Double Heading Prevention
|
||||||
|
|
||||||
@@ -268,7 +276,7 @@ wp-bootstrap (parent theme, Bootstrap 5 FSE + Twig rendering)
|
|||||||
### Important Constraints
|
### Important Constraints
|
||||||
|
|
||||||
- **WooCommerce plugin is read-only.** We have no control over its source code. All customizations happen in the child theme via template overrides and hooks.
|
- **WooCommerce plugin is read-only.** We have no control over its source code. All customizations happen in the child theme via template overrides and hooks.
|
||||||
- **Docker environment is not yet set up.** Commands referencing `docker exec` (e.g., in the translation workflow) are not currently available. Local alternatives or manual steps must be used until the container is configured.
|
- **Docker environment:** Container name is `woocommerce`. Use `docker exec woocommerce ...` for commands and `docker exec woocommerce apache2ctl graceful` to clear OPcache after PHP changes.
|
||||||
|
|
||||||
### Cross-Project Workflow
|
### Cross-Project Workflow
|
||||||
|
|
||||||
@@ -299,7 +307,11 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr
|
|||||||
|
|
||||||
## Architecture Decisions
|
## Architecture Decisions
|
||||||
|
|
||||||
- **Edit forms** use 3+9 column layout: `col-lg-3` sticky sidebar (progress indicator, section nav) + `col-lg-9` card-based sections.
|
- **My Account layout** uses `col-lg-auto` (nav) + `col-lg` (content) so the nav auto-sizes to its content and the main area fills remaining space. Requires CSS resets for WooCommerce's float-based layout (`float: none; width: auto/100%`) and `max-width: none` on `.woocommerce-account main .woocommerce` to override the plugin's `max-width: 1000px`.
|
||||||
|
- **My Account navigation** uses `offcanvas-lg offcanvas-start` responsive pattern: full sidebar with icons on desktop (sticky via `position-sticky`), offcanvas slide-in on mobile with toggle button. Icons are mapped per endpoint via a Twig hash.
|
||||||
|
- **My Account dashboard** uses card-based quick actions (`row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3`) with `bg-primary-subtle` icon containers that adapt to dark mode. Each action card is an `<a class="card">` for full-card clickability.
|
||||||
|
- **My Account forms** (edit-account, edit-address, lost-password, reset-password) are wrapped in `card shadow-sm` sections with icon headers for visual consistency.
|
||||||
|
- **Edit forms** use card-based section grouping (e.g., "Personal information" + "Password change" as separate cards) instead of `<fieldset>/<legend>`.
|
||||||
- **Detail pages** use 8+4 column layout with sticky sidebar.
|
- **Detail pages** use 8+4 column layout with sticky sidebar.
|
||||||
- **Cards** use `<article class="card h-100">` with `stretched-link`.
|
- **Cards** use `<article class="card h-100">` with `stretched-link`.
|
||||||
- **Form layout** uses centered `col-lg-8 col-xl-7` with card + shadow for auth forms.
|
- **Form layout** uses centered `col-lg-8 col-xl-7` with card + shadow for auth forms.
|
||||||
@@ -309,8 +321,69 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
Current version: **v0.0.1**
|
Current version: **v0.1.0**
|
||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
<!-- AI assistants: document key learnings and session outcomes here -->
|
### 2026-02-28 — My Account Bootstrap 5 Polish
|
||||||
|
|
||||||
|
**Scope:** Redesigned 8 my-account Twig templates + CSS overrides to feel like a polished Bootstrap 5 application.
|
||||||
|
|
||||||
|
**Files changed (10):**
|
||||||
|
|
||||||
|
- `templates/myaccount/navigation.html.twig` — Added endpoint icon map, `offcanvas-lg` responsive pattern, sticky sidebar
|
||||||
|
- `templates/myaccount/dashboard.html.twig` — Replaced plain `<p>` tags with welcome card (avatar + greeting) and 5 quick-action cards in responsive grid
|
||||||
|
- `templates/myaccount/view-order.html.twig` — Replaced `<mark>` tags with summary card using `list-group-flush` and `components/status-badge.html.twig`; notes wrapped in card
|
||||||
|
- `templates/myaccount/form-edit-account.html.twig` — Wrapped in two card sections (Personal info + Password change) with icons
|
||||||
|
- `templates/myaccount/form-edit-address.html.twig` — Wrapped in card with dynamic icon (`bi-receipt` billing / `bi-truck` shipping)
|
||||||
|
- `templates/myaccount/form-lost-password.html.twig` — Wrapped in card with `bi-key` icon
|
||||||
|
- `templates/myaccount/form-reset-password.html.twig` — Wrapped in card with `bi-shield-lock` icon
|
||||||
|
- `templates/myaccount/lost-password-confirmation.html.twig` — Added `text-body-secondary` styling and "Back to login" button
|
||||||
|
- `templates/myaccount/my-account.html.twig` — Changed grid from `col-lg-3`/`col-lg-9` to `col-lg-auto`/`col-lg`
|
||||||
|
- `assets/css/wc-bootstrap.css` — Reset WooCommerce float layout, override `max-width: 1000px`, avatar rounding, card hover lift
|
||||||
|
|
||||||
|
**Design decisions:**
|
||||||
|
|
||||||
|
- **Nav column auto-sizing (`col-lg-auto`)**: Fixed-width columns (e.g., `col-lg-3` = 285px) caused label overflow with icons. Auto-sizing lets the nav take exactly the space it needs across locales while the content fills the rest.
|
||||||
|
- **WooCommerce layout overrides require matching specificity**: The plugin uses `.woocommerce-account .woocommerce-MyAccount-navigation` (specificity `0,2,0`) — single-class selectors don't override. Also `.woocommerce-account main .woocommerce` sets `max-width: 1000px` (specificity `0,2,1`) which must be reset to `none`.
|
||||||
|
- **`offcanvas-lg` over `d-none d-lg-block`**: Bootstrap's responsive offcanvas natively handles the desktop/mobile switch without duplicating nav markup. The toggle button uses `d-lg-none` visibility.
|
||||||
|
- **`bg-primary-subtle` for icon containers**: These Bootstrap 5.3 contextual utilities automatically adapt to dark mode, unlike hardcoded colors.
|
||||||
|
- **Welcome message restructured**: Separated greeting from logout link instead of using WooCommerce's default inline-linked `__()` string. This gives full control over card layout and avoids translated strings containing HTML structure assumptions.
|
||||||
|
- **Templates NOT changed** (already well-done): `orders.html.twig`, `my-address.html.twig`, `form-login.html.twig`, `payment-methods.html.twig`, `form-add-payment-method.html.twig`, `downloads.html.twig`
|
||||||
|
|
||||||
|
### 2026-02-28 — Single Product Bootstrap 5 Layout + Template Quirks Audit
|
||||||
|
|
||||||
|
**Scope:** Created Bootstrap 5 two-column layout for single product pages. Then audited all ~90 Twig templates for WooCommerce CSS quirks, Twig escaping bugs, and missing Bootstrap styling.
|
||||||
|
|
||||||
|
**Single product layout (3 files):**
|
||||||
|
|
||||||
|
- `woocommerce/content-single-product.php` — Bridge file for `wc_get_template_part()` interception
|
||||||
|
- `templates/content-single-product.html.twig` — Two-column `row gx-4 gx-lg-5` grid (images left, summary right)
|
||||||
|
- `assets/css/wc-bootstrap.css` — Float/width reset, sale badge positioning, shop_table border reset, gallery opacity fallback
|
||||||
|
|
||||||
|
**Template quirks audit (14 files fixed):**
|
||||||
|
|
||||||
|
- `order/order-details.html.twig` — Fixed `|nl2br|esc_html` filter order (was escaping `<br>` tags)
|
||||||
|
- `single-product/product-image.html.twig` — Added WC gallery modifier classes + opacity:0 for JS init
|
||||||
|
- `brands/brand-description.html.twig` — Added `|raw` to HTML-producing filter chain
|
||||||
|
- `single-product/up-sells.html.twig`, `cart/cross-sells.html.twig`, `single-product/related.html.twig` — Fixed `…` double-encoding in headings
|
||||||
|
- `myaccount/dashboard.html.twig` — Removed duplicate deprecated hook fires
|
||||||
|
- `product-searchform.html.twig` — Replaced `…` entity with Unicode `…`
|
||||||
|
- `cart/cart-totals.html.twig`, `checkout/review-order.html.twig`, `checkout/form-login.html.twig`, `checkout/terms.html.twig` — Removed wrong `function() is defined` guards
|
||||||
|
- `wc-base.html.twig` — Added `|esc_attr` on notification type in class attribute
|
||||||
|
- `global/form-login.html.twig` — Added `is defined` guard on `hidden` variable
|
||||||
|
- `single-product/add-to-cart/variation.html.twig` — Added `role="alert"` for accessibility
|
||||||
|
- `cart/mini-cart.html.twig` — Changed remove link to Bootstrap `btn btn-sm btn-outline-danger`
|
||||||
|
- `cart/cart-shipping.html.twig` — Made `form-check` class conditional on multiple shipping methods
|
||||||
|
|
||||||
|
**Infrastructure:**
|
||||||
|
|
||||||
|
- Created `.claude/settings.json` with allowed commands (git, docker, composer, npm, etc.)
|
||||||
|
|
||||||
|
### 2026-02-28 — v0.1.0 Changelog & Re-Release
|
||||||
|
|
||||||
|
**Scope:** Added CHANGELOG.md entry for v0.1.0 and re-released the tag.
|
||||||
|
|
||||||
|
**Files changed (1):**
|
||||||
|
|
||||||
|
- `CHANGELOG.md` — Added `[0.1.0]` section documenting all features and fixes between v0.0.1 and v0.1.0
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -64,7 +64,12 @@ wc-bootstrap/
|
|||||||
│ └── WooCommerceExtension.php # WC/WP Twig functions & filters
|
│ └── WooCommerceExtension.php # WC/WP Twig functions & filters
|
||||||
├── languages/ # Translation files
|
├── languages/ # Translation files
|
||||||
├── templates/ # Bootstrap 5 Twig template overrides
|
├── templates/ # Bootstrap 5 Twig template overrides
|
||||||
|
│ ├── wc-base.html.twig # Base layout (extends parent's base.html.twig)
|
||||||
|
│ ├── layouts/ # Page-type layouts (account, archive, form, page, single)
|
||||||
│ ├── archive-product.html.twig
|
│ ├── archive-product.html.twig
|
||||||
|
│ ├── content-product.html.twig
|
||||||
|
│ ├── content-product-cat.html.twig
|
||||||
|
│ ├── product-searchform.html.twig
|
||||||
│ ├── auth/
|
│ ├── auth/
|
||||||
│ ├── brands/
|
│ ├── brands/
|
||||||
│ ├── cart/
|
│ ├── cart/
|
||||||
@@ -76,6 +81,13 @@ wc-bootstrap/
|
|||||||
│ ├── notices/
|
│ ├── notices/
|
||||||
│ ├── order/
|
│ ├── order/
|
||||||
│ └── single-product/
|
│ └── single-product/
|
||||||
|
├── docker/
|
||||||
|
│ ├── Dockerfile # Multistage build (WC download, npm, composer, WP)
|
||||||
|
│ ├── entrypoint.sh # Auto-setup wrapper entrypoint
|
||||||
|
│ └── setup.sh # First-run WP install + plugin/theme activation
|
||||||
|
├── .env-dist # Environment variable template
|
||||||
|
├── compose.yaml # WordPress + MariaDB services
|
||||||
|
├── compose.override.yaml # Dev overrides (bind mounts, debug flags)
|
||||||
├── composer.json
|
├── composer.json
|
||||||
├── functions.php
|
├── functions.php
|
||||||
└── style.css
|
└── style.css
|
||||||
@@ -95,6 +107,29 @@ WooCommerce wc_get_template("cart/cart.php")
|
|||||||
→ Result: Bootstrap 5 Twig output only
|
→ Result: Bootstrap 5 Twig output only
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy and adjust environment variables (optional)
|
||||||
|
cp .env-dist .env
|
||||||
|
|
||||||
|
# Build and start
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# WordPress installs automatically on first boot (admin/admin)
|
||||||
|
# → http://localhost:8080
|
||||||
|
# → http://localhost:8080/wp-admin/
|
||||||
|
|
||||||
|
# Manual setup (if WP_AUTO_SETUP=0)
|
||||||
|
docker compose exec wordpress setup.sh
|
||||||
|
|
||||||
|
# Tear down
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
The image preinstalls WooCommerce and wp-bootstrap. The override bind-mounts both themes for live development.
|
||||||
|
|
||||||
### Building Translations
|
### Building Translations
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -109,12 +109,113 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Product link wrapping card content — remove underline, inherit text color */
|
||||||
|
.product.card a.woocommerce-LoopProduct-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product title in card body */
|
||||||
|
.product.card .woocommerce-loop-product__title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push price to bottom of card body for even card heights */
|
||||||
|
.product.card .card-body .price {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add-to-cart button — Bootstrap btn-outline-primary style */
|
||||||
|
.product.card .button {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
color: var(--bs-primary);
|
||||||
|
border: var(--bs-border-width) solid var(--bs-primary);
|
||||||
|
background-color: transparent;
|
||||||
|
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product.card .button:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "View options" button for variable products */
|
||||||
|
.product.card .button.product_type_variable {
|
||||||
|
color: var(--bs-secondary);
|
||||||
|
border-color: var(--bs-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product.card .button.product_type_variable:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--bs-secondary);
|
||||||
|
border-color: var(--bs-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Read more" button for external/non-purchasable products */
|
||||||
|
.product.card .button.product_type_external {
|
||||||
|
color: var(--bs-info);
|
||||||
|
border-color: var(--bs-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product.card .button.product_type_external:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--bs-info);
|
||||||
|
border-color: var(--bs-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Added-to-cart visual feedback */
|
||||||
|
.product.card .added_to_cart {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: var(--bs-success);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WooCommerce result count and ordering bar */
|
||||||
|
.woocommerce-result-count {
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-ordering select {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Sale Badge
|
Sale Badge
|
||||||
Positioning for the sale overlay badge on product cards.
|
Position inside gallery area, not overlapping breadcrumb.
|
||||||
|
WooCommerce sets top: -.5em which bleeds above the containing block.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
.onsale {
|
.woocommerce span.onsale {
|
||||||
|
top: 0.5em;
|
||||||
|
left: 0.5em;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +266,18 @@
|
|||||||
border-radius: var(--bs-border-radius);
|
border-radius: var(--bs-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Single Product Layout
|
||||||
|
Reset WooCommerce's float-based two-column layout for single product pages.
|
||||||
|
Bootstrap's row/col-lg-6 grid handles the layout instead.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce div.product div.images,
|
||||||
|
.woocommerce div.product div.summary {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Variation Selectors
|
Variation Selectors
|
||||||
Spacing for variable product attribute dropdowns.
|
Spacing for variable product attribute dropdowns.
|
||||||
@@ -179,12 +292,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
WooCommerce Grid Override
|
Shop / Archive Layout
|
||||||
Reset WooCommerce's default grid to let Bootstrap handle layout.
|
Reset WooCommerce's default float grid to let Bootstrap handle layout.
|
||||||
|
Override woocommerce-layout.css float-based widths and clearfixes.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
.woocommerce ul.products {
|
/* Reset float-based result count / ordering bar — use flexbox instead */
|
||||||
display: contents;
|
.post-type-archive-product .woocommerce-result-count,
|
||||||
|
.tax-product_cat .woocommerce-result-count,
|
||||||
|
.tax-product_tag .woocommerce-result-count,
|
||||||
|
.post-type-archive-product .woocommerce-ordering,
|
||||||
|
.tax-product_cat .woocommerce-ordering,
|
||||||
|
.tax-product_tag .woocommerce-ordering {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset WooCommerce's product grid floats and widths */
|
||||||
|
.woocommerce ul.products,
|
||||||
|
.woocommerce .products {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce ul.products li.product,
|
||||||
|
.woocommerce .products .product {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -280,16 +418,50 @@ header.sticky-top.is-stuck {
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
My Account
|
My Account
|
||||||
Navigation and layout for the My Account area.
|
Navigation and layout for the My Account area.
|
||||||
|
Reset WooCommerce's float-based layout to let Bootstrap flex grid handle it.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce-account main .woocommerce,
|
||||||
|
.woocommerce-cart main .woocommerce,
|
||||||
|
.woocommerce-checkout main .woocommerce,
|
||||||
|
.post-type-archive-product main .woocommerce,
|
||||||
|
.tax-product_cat main .woocommerce,
|
||||||
|
.tax-product_tag main .woocommerce {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-account .woocommerce-MyAccount-navigation {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-account .woocommerce-MyAccount-content {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-MyAccount-navigation .list-group-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-MyAccount-navigation .list-group-item.active {
|
.woocommerce-MyAccount-navigation .list-group-item.active {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Order status marks */
|
/* Dashboard: avatar rounding */
|
||||||
.woocommerce-order-details mark {
|
.woocommerce-MyAccount-content .avatar {
|
||||||
background: none;
|
border-radius: 50%;
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
/* Dashboard: quick action card hover lift */
|
||||||
|
.woocommerce-MyAccount-content a.card {
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-MyAccount-content a.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--bs-box-shadow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* View-order notes */
|
/* View-order notes */
|
||||||
@@ -345,3 +517,30 @@ header.sticky-top.is-stuck {
|
|||||||
.product-quantity {
|
.product-quantity {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Shop Table
|
||||||
|
Reset WooCommerce's border styles on .shop_table to let Bootstrap's
|
||||||
|
.table class handle borders via --bs-table-* custom properties.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce table.shop_table {
|
||||||
|
border: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce table.shop_table td,
|
||||||
|
.woocommerce table.shop_table th {
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Product Gallery
|
||||||
|
WooCommerce JS sets opacity: 1 after initialization. Ensure the gallery
|
||||||
|
is visible even if WC JS doesn't run (e.g., gallery features disabled).
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce-product-gallery--without-images {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|||||||
17
compose.override.yaml
Normal file
17
compose.override.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: woocommerce-db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
wordpress:
|
||||||
|
container_name: woocommerce
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ../wp-bootstrap:/var/www/html/wp-content/themes/wp-bootstrap
|
||||||
|
- .:/var/www/html/wp-content/themes/wc-bootstrap
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DEBUG: "1"
|
||||||
|
WORDPRESS_CONFIG_EXTRA: |
|
||||||
|
define('WP_DEBUG_LOG', true);
|
||||||
|
define('WP_DEBUG_DISPLAY', true);
|
||||||
|
define('SCRIPT_DEBUG', true);
|
||||||
48
compose.yaml
Normal file
48
compose.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:latest
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-wordpress}
|
||||||
|
MYSQL_USER: ${MYSQL_USER:-wordpress}
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-wordpress}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
wordpress:
|
||||||
|
image: hub.bundespruefstelle.ch/woocommerce:latest
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
additional_contexts:
|
||||||
|
wp-bootstrap: ../wp-bootstrap
|
||||||
|
target: wp_runtime
|
||||||
|
ports:
|
||||||
|
- "${WP_PORT:-8080}:80"
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db
|
||||||
|
WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
|
||||||
|
WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
|
||||||
|
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wordpress}
|
||||||
|
WP_AUTO_SETUP: ${WP_AUTO_SETUP:-1}
|
||||||
|
WP_URL: ${WP_URL:-http://localhost:${WP_PORT:-8080}}
|
||||||
|
WP_TITLE: ${WP_TITLE:-WC Bootstrap Dev}
|
||||||
|
WP_ADMIN_USER: ${WP_ADMIN_USER:-admin}
|
||||||
|
WP_ADMIN_PASSWORD: ${WP_ADMIN_PASSWORD:-admin}
|
||||||
|
WP_ADMIN_EMAIL: ${WP_ADMIN_EMAIL:-admin@example.com}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- wp_data:/var/www/html
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
wp_data:
|
||||||
70
docker/Dockerfile
Normal file
70
docker/Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Stage 1 — Download WooCommerce from WordPress.org
|
||||||
|
###############################################################################
|
||||||
|
FROM alpine:3.21 AS woocommerce
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl unzip jq
|
||||||
|
|
||||||
|
ARG WOOCOMMERCE_VERSION=latest
|
||||||
|
RUN set -ex; \
|
||||||
|
if [ "$WOOCOMMERCE_VERSION" = "latest" ]; then \
|
||||||
|
URL=$(curl -sSf "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=woocommerce" \
|
||||||
|
| jq -r '.download_link'); \
|
||||||
|
else \
|
||||||
|
URL="https://downloads.wordpress.org/plugin/woocommerce.${WOOCOMMERCE_VERSION}.zip"; \
|
||||||
|
fi; \
|
||||||
|
curl -sSfL -o /tmp/woocommerce.zip "$URL"; \
|
||||||
|
unzip -q /tmp/woocommerce.zip -d /tmp/plugins; \
|
||||||
|
rm /tmp/woocommerce.zip
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Stage 2 — Build wp-bootstrap theme assets
|
||||||
|
###############################################################################
|
||||||
|
FROM node:20-alpine AS wp-bootstrap-assets
|
||||||
|
|
||||||
|
COPY --from=wp-bootstrap . /build
|
||||||
|
WORKDIR /build
|
||||||
|
RUN npm ci && npm run build \
|
||||||
|
&& rm -rf node_modules src .git .gitea .github .vscode .claude .idea \
|
||||||
|
CLAUDE.md PLAN.md package.json package-lock.json .editorconfig \
|
||||||
|
releases .gitignore
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Stage 3 — Install Composer dependencies for both themes
|
||||||
|
###############################################################################
|
||||||
|
FROM composer:2 AS composer
|
||||||
|
|
||||||
|
COPY --from=wp-bootstrap-assets /build /themes/wp-bootstrap
|
||||||
|
WORKDIR /themes/wp-bootstrap
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
COPY . /themes/wc-bootstrap
|
||||||
|
WORKDIR /themes/wc-bootstrap
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Stage 4 — Final WordPress image
|
||||||
|
###############################################################################
|
||||||
|
FROM wordpress:php8.4 AS wp_runtime
|
||||||
|
|
||||||
|
RUN curl -sSfL -o /usr/local/bin/wp \
|
||||||
|
https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
|
||||||
|
&& chmod +x /usr/local/bin/wp
|
||||||
|
|
||||||
|
COPY --from=woocommerce /tmp/plugins/woocommerce \
|
||||||
|
/var/www/html/wp-content/plugins/woocommerce
|
||||||
|
|
||||||
|
COPY --from=composer /themes/wp-bootstrap \
|
||||||
|
/var/www/html/wp-content/themes/wp-bootstrap
|
||||||
|
|
||||||
|
COPY --from=composer /themes/wc-bootstrap \
|
||||||
|
/var/www/html/wp-content/themes/wc-bootstrap
|
||||||
|
|
||||||
|
COPY --link --chmod=0755 docker/setup.sh /usr/local/bin/setup.sh
|
||||||
|
COPY --link --chmod=0755 docker/entrypoint.sh /usr/local/bin/wc-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["wc-entrypoint.sh"]
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
21
docker/entrypoint.sh
Normal file
21
docker/entrypoint.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Wrapper entrypoint: runs first-time WordPress setup in the background,
|
||||||
|
# then hands off to the official WordPress entrypoint.
|
||||||
|
#
|
||||||
|
# Set WP_AUTO_SETUP=0 to disable automatic setup.
|
||||||
|
#
|
||||||
|
|
||||||
|
if [ "${WP_AUTO_SETUP:-1}" = "1" ]; then
|
||||||
|
(
|
||||||
|
cd /var/www/html
|
||||||
|
until [ -f wp-config.php ]; do sleep 2; done
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if ! wp core is-installed --allow-root 2>/dev/null; then
|
||||||
|
setup.sh
|
||||||
|
fi
|
||||||
|
) &
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec docker-entrypoint.sh "$@"
|
||||||
47
docker/setup.sh
Normal file
47
docker/setup.sh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# First-run setup: installs WordPress, activates WooCommerce and wc-bootstrap.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker compose exec wordpress setup.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WP_URL="${WP_URL:-http://localhost:8080}"
|
||||||
|
WP_TITLE="${WP_TITLE:-WC Bootstrap Dev}"
|
||||||
|
WP_ADMIN_USER="${WP_ADMIN_USER:-admin}"
|
||||||
|
WP_ADMIN_PASSWORD="${WP_ADMIN_PASSWORD:-admin}"
|
||||||
|
WP_ADMIN_EMAIL="${WP_ADMIN_EMAIL:-admin@example.com}"
|
||||||
|
|
||||||
|
cd /var/www/html
|
||||||
|
|
||||||
|
# Wait for WordPress files (entrypoint copies them on first boot)
|
||||||
|
until [ -f wp-config.php ]; do
|
||||||
|
echo "Waiting for WordPress to initialize..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! wp core is-installed --allow-root 2>/dev/null; then
|
||||||
|
echo "Installing WordPress..."
|
||||||
|
wp core install \
|
||||||
|
--url="$WP_URL" \
|
||||||
|
--title="$WP_TITLE" \
|
||||||
|
--admin_user="$WP_ADMIN_USER" \
|
||||||
|
--admin_password="$WP_ADMIN_PASSWORD" \
|
||||||
|
--admin_email="$WP_ADMIN_EMAIL" \
|
||||||
|
--skip-email \
|
||||||
|
--allow-root
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Activating WooCommerce..."
|
||||||
|
wp plugin activate woocommerce --allow-root 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Activating wc-bootstrap theme..."
|
||||||
|
wp theme activate wc-bootstrap --allow-root 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Setup complete!"
|
||||||
|
echo " URL: $WP_URL"
|
||||||
|
echo " Admin: $WP_URL/wp-admin/"
|
||||||
|
echo " User: $WP_ADMIN_USER"
|
||||||
|
echo " Password: $WP_ADMIN_PASSWORD"
|
||||||
210
functions.php
210
functions.php
@@ -14,6 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define theme constants.
|
* Define theme constants.
|
||||||
*
|
*
|
||||||
@@ -22,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
* 2. This PHP constant — used internally by the theme
|
* 2. This PHP constant — used internally by the theme
|
||||||
* Both MUST be updated on every release.
|
* Both MUST be updated on every release.
|
||||||
*/
|
*/
|
||||||
define( 'WC_BOOTSTRAP_VERSION', '0.0.1' );
|
define( 'WC_BOOTSTRAP_VERSION', '0.1.0' );
|
||||||
define( 'WC_BOOTSTRAP_PATH', get_stylesheet_directory() . '/' );
|
define( 'WC_BOOTSTRAP_PATH', get_stylesheet_directory() . '/' );
|
||||||
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
|
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ function wc_bootstrap_enqueue_styles(): void {
|
|||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'wc-bootstrap-overrides',
|
'wc-bootstrap-overrides',
|
||||||
get_stylesheet_directory_uri() . '/assets/css/wc-bootstrap.css',
|
get_stylesheet_directory_uri() . '/assets/css/wc-bootstrap.css',
|
||||||
array( 'wc-bootstrap-style', 'woocommerce' ),
|
array( 'wc-bootstrap-style', 'woocommerce-general' ),
|
||||||
$theme_version
|
$theme_version
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -223,3 +224,208 @@ function wc_bootstrap_sticky_header_script(): void {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
add_action( 'wp_footer', 'wc_bootstrap_sticky_header_script' );
|
add_action( 'wp_footer', 'wc_bootstrap_sticky_header_script' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the shop sidebar widget area.
|
||||||
|
*
|
||||||
|
* Provides a widget area for product filters and shop-specific widgets.
|
||||||
|
* Uses Bootstrap-styled markup matching the parent theme's sidebar pattern.
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_register_sidebars(): void {
|
||||||
|
register_sidebar( array(
|
||||||
|
'name' => __( 'Shop Sidebar', 'wc-bootstrap' ),
|
||||||
|
'id' => 'shop-sidebar',
|
||||||
|
'description' => __( 'Add widgets here to appear in the shop sidebar.', 'wc-bootstrap' ),
|
||||||
|
'before_widget' => '<div id="%1$s" class="widget mb-4 %2$s">',
|
||||||
|
'after_widget' => '</div>',
|
||||||
|
'before_title' => '<h3 class="sidebar-heading h6 text-uppercase fw-semibold">',
|
||||||
|
'after_title' => '</h3>',
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
add_action( 'widgets_init', 'wc_bootstrap_register_sidebars' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of product columns in the shop loop.
|
||||||
|
*
|
||||||
|
* @return int Number of columns.
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_loop_columns(): int {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove WooCommerce's default sidebar hook.
|
||||||
|
*
|
||||||
|
* The child theme's archive-product.php renders the sidebar inline within the
|
||||||
|
* Bootstrap grid layout, so the default woocommerce_sidebar hook must not render
|
||||||
|
* a second sidebar outside the layout.
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_remove_default_sidebar(): void {
|
||||||
|
remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );
|
||||||
|
}
|
||||||
|
add_action( 'init', 'wc_bootstrap_remove_default_sidebar' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace WooCommerce's content wrapper with a no-op.
|
||||||
|
*
|
||||||
|
* The parent theme's page shell already wraps content in a .container,
|
||||||
|
* so WooCommerce's default wrapper (another .container + #primary + <main>)
|
||||||
|
* creates a double-nested container that constrains width. Remove it and
|
||||||
|
* let the parent theme handle the outer layout.
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_remove_content_wrappers(): void {
|
||||||
|
remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
|
||||||
|
remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );
|
||||||
|
}
|
||||||
|
add_action( 'init', 'wc_bootstrap_remove_content_wrappers' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent the parent theme from rendering WooCommerce pages.
|
||||||
|
*
|
||||||
|
* The parent theme's TemplateController hooks template_redirect at priority 10
|
||||||
|
* and renders its own templates for all requests (then exits). For WooCommerce
|
||||||
|
* pages we need to render our own Bootstrap layouts instead.
|
||||||
|
* Returning false tells the parent theme to skip rendering for this request.
|
||||||
|
*
|
||||||
|
* @param bool $should_render Whether the parent theme should render.
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_skip_parent_template( bool $should_render ): bool {
|
||||||
|
if ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( function_exists( 'is_product_taxonomy' ) && is_product_taxonomy() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( function_exists( 'is_product' ) && is_product() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $should_render;
|
||||||
|
}
|
||||||
|
add_filter( 'wp_bootstrap_should_render_template', 'wc_bootstrap_skip_parent_template' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable WooCommerce's block template compatibility layer.
|
||||||
|
*
|
||||||
|
* The parent theme has theme.json which makes wp_is_block_theme() return true.
|
||||||
|
* WooCommerce detects this and removes classic template hooks (title, price,
|
||||||
|
* add-to-cart, etc.) from single product and archive pages, expecting blocks
|
||||||
|
* to handle rendering instead. Since we render via classic hooks + Twig, we
|
||||||
|
* need the hooks to stay registered.
|
||||||
|
*
|
||||||
|
* @param bool $disabled Whether the compatibility layer is disabled.
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_disable_block_compatibility( bool $disabled ): bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
add_filter( 'woocommerce_disable_compatibility_layer', 'wc_bootstrap_disable_block_compatibility' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render product archive pages with Bootstrap 5 layout.
|
||||||
|
*
|
||||||
|
* Since the parent theme's TemplateController is blocked for product archives
|
||||||
|
* (via wp_bootstrap_should_render_template filter), we render the page ourselves
|
||||||
|
* at priority 11 using the parent theme's TwigService and page shell.
|
||||||
|
*
|
||||||
|
* The archive-product.php file provides the Bootstrap layout (sidebar + product
|
||||||
|
* grid) and is captured via output buffering, then injected into the parent
|
||||||
|
* theme's page template — the same pattern as wc_bootstrap_render_page().
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_render_product_archive(): void {
|
||||||
|
$is_shop = is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) );
|
||||||
|
$is_tax = function_exists( 'is_product_taxonomy' ) && is_product_taxonomy();
|
||||||
|
|
||||||
|
if ( ! $is_shop && ! $is_tax ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|
||||||
|
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture WooCommerce archive content via output buffering.
|
||||||
|
ob_start();
|
||||||
|
include get_stylesheet_directory() . '/woocommerce/archive-product.php';
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
// Build parent theme context and inject archive content into page shell.
|
||||||
|
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||||
|
$theme_context = $context_builder->build();
|
||||||
|
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||||
|
|
||||||
|
$theme_context['post'] = array_merge(
|
||||||
|
$theme_context['post'] ?? [],
|
||||||
|
[
|
||||||
|
'content' => $content,
|
||||||
|
'title' => '',
|
||||||
|
'thumbnail' => '',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single product pages with Bootstrap 5 layout.
|
||||||
|
*
|
||||||
|
* Since the parent theme's TemplateController is blocked for single products
|
||||||
|
* (via wp_bootstrap_should_render_template filter), we render the page ourselves
|
||||||
|
* at priority 11 using the parent theme's TwigService and page shell.
|
||||||
|
*
|
||||||
|
* The single-product.php file fires WooCommerce hooks and is captured via output
|
||||||
|
* buffering, then injected into the parent theme's page template — the same
|
||||||
|
* pattern as wc_bootstrap_render_product_archive().
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
function wc_bootstrap_render_single_product(): void {
|
||||||
|
if ( ! function_exists( 'is_product' ) || ! is_product() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|
||||||
|
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture WooCommerce single product content via output buffering.
|
||||||
|
ob_start();
|
||||||
|
include get_stylesheet_directory() . '/woocommerce/single-product.php';
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
// Build parent theme context and inject product content into page shell.
|
||||||
|
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||||
|
$theme_context = $context_builder->build();
|
||||||
|
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||||
|
|
||||||
|
$theme_context['post'] = array_merge(
|
||||||
|
$theme_context['post'] ?? [],
|
||||||
|
[
|
||||||
|
'content' => $content,
|
||||||
|
'title' => '',
|
||||||
|
'thumbnail' => '',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 );
|
||||||
|
|||||||
@@ -79,7 +79,16 @@ class TemplateOverride {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$twig = TwigService::getInstance();
|
$twig = TwigService::getInstance();
|
||||||
echo $twig->render( $twigTemplate, $args );
|
$context = $args;
|
||||||
|
|
||||||
|
// Inject the global $product into the Twig context.
|
||||||
|
// WooCommerce PHP templates access it via `global $product;` but Twig
|
||||||
|
// templates have isolated variable scopes and need it passed explicitly.
|
||||||
|
if ( ! isset( $context['product'] ) && ! empty( $GLOBALS['product'] ) ) {
|
||||||
|
$context['product'] = $GLOBALS['product'];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $twig->render( $twigTemplate, $context );
|
||||||
|
|
||||||
// Buffer the upcoming PHP include so we can discard it.
|
// Buffer the upcoming PHP include so we can discard it.
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCo
|
|||||||
Requires at least: 6.7
|
Requires at least: 6.7
|
||||||
Tested up to: 6.7
|
Tested up to: 6.7
|
||||||
Requires PHP: 8.3
|
Requires PHP: 8.3
|
||||||
Version: 0.0.1
|
Version: 0.1.0
|
||||||
License: GNU General Public License v2 or later
|
License: GNU General Public License v2 or later
|
||||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||||
Template: wp-bootstrap
|
Template: wp-bootstrap
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ do_action('woocommerce_before_main_content') }}
|
{{ do_action('woocommerce_before_main_content') }}
|
||||||
|
|||||||
@@ -24,6 +24,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{ term_description()|wptexturize|wpautop|do_shortcode }}
|
{{ term_description()|wptexturize|wpautop|do_shortcode|raw }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
{% if available_methods is defined and available_methods|length > 0 %}
|
{% if available_methods is defined and available_methods|length > 0 %}
|
||||||
<ul id="shipping_method_{{ index|default(0) }}" class="list-unstyled mb-2">
|
<ul id="shipping_method_{{ index|default(0) }}" class="list-unstyled mb-2">
|
||||||
{% for method_id, method in available_methods %}
|
{% for method_id, method in available_methods %}
|
||||||
<li class="form-check">
|
<li{% if available_methods|length > 1 %} class="form-check"{% endif %}>
|
||||||
{% if available_methods|length > 1 %}
|
{% if available_methods|length > 1 %}
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name="shipping_method[{{ index|default(0) }}]"
|
name="shipping_method[{{ index|default(0) }}]"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
class="shipping_method"
|
class="shipping_method"
|
||||||
data-index="{{ index|default(0) }}" />
|
data-index="{{ index|default(0) }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<label class="form-check-label" for="shipping_method_{{ index|default(0) }}_{{ method_id|esc_attr }}">
|
<label class="{{ available_methods|length > 1 ? 'form-check-label' : '' }}" for="shipping_method_{{ index|default(0) }}_{{ method_id|esc_attr }}">
|
||||||
{{ method.get_label()|raw }}
|
{{ method.get_label()|raw }}
|
||||||
</label>
|
</label>
|
||||||
{{ do_action('woocommerce_after_shipping_rate', method, index|default(0)) }}
|
{{ do_action('woocommerce_after_shipping_rate', method, index|default(0)) }}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{# Shipping #}
|
{# Shipping #}
|
||||||
{{ do_action('woocommerce_cart_totals_before_shipping') }}
|
{{ do_action('woocommerce_cart_totals_before_shipping') }}
|
||||||
|
|
||||||
{% if wc_shipping_enabled() is defined and wc_shipping_enabled() %}
|
{% if wc_shipping_enabled() %}
|
||||||
<li class="list-group-item cart-shipping">
|
<li class="list-group-item cart-shipping">
|
||||||
{{ do_action('woocommerce_cart_totals_shipping') }}
|
{{ do_action('woocommerce_cart_totals_shipping') }}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<section class="cross-sells mt-5">
|
<section class="cross-sells mt-5">
|
||||||
{% set heading = heading|default(__('You may be interested in…')) %}
|
{% set heading = heading|default(__('You may be interested in…')) %}
|
||||||
{% if heading %}
|
{% if heading %}
|
||||||
<h2 class="h4 mb-4">{{ heading }}</h2>
|
<h2 class="h4 mb-4">{{ heading|raw }}</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ woocommerce_product_loop_start() }}
|
{{ woocommerce_product_loop_start() }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<li class="woocommerce-mini-cart-item d-flex gap-3 py-2 border-bottom {{ item.css_class|default('') }}">
|
<li class="woocommerce-mini-cart-item d-flex gap-3 py-2 border-bottom {{ item.css_class|default('') }}">
|
||||||
{# Remove link #}
|
{# Remove link #}
|
||||||
<a href="{{ item.remove_url|esc_url }}"
|
<a href="{{ item.remove_url|esc_url }}"
|
||||||
class="remove remove_from_cart_button text-danger"
|
class="btn btn-sm btn-outline-danger remove remove_from_cart_button"
|
||||||
aria-label="{{ __('Remove this item') }}"
|
aria-label="{{ __('Remove this item') }}"
|
||||||
data-product_id="{{ item.product_id }}"
|
data-product_id="{{ item.product_id }}"
|
||||||
data-cart_item_key="{{ item.key }}">
|
data-cart_item_key="{{ item.key }}">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% if is_user_logged_in() is defined and not is_user_logged_in() %}
|
{% if not is_user_logged_in() %}
|
||||||
<div class="woocommerce-form-login-toggle mb-4">
|
<div class="woocommerce-form-login-toggle mb-4">
|
||||||
<div class="alert alert-info d-flex align-items-center" role="status">
|
<div class="alert alert-info d-flex align-items-center" role="status">
|
||||||
<i class="bi bi-person me-2" aria-hidden="true"></i>
|
<i class="bi bi-person me-2" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_review_order_before_shipping') }}
|
{{ do_action('woocommerce_review_order_before_shipping') }}
|
||||||
|
|
||||||
{% if wc_shipping_enabled() is defined and wc_shipping_enabled() %}
|
{% if wc_shipping_enabled() %}
|
||||||
{{ do_action('woocommerce_review_order_shipping') }}
|
{{ do_action('woocommerce_review_order_shipping') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_checkout_before_terms_and_conditions') }}
|
{{ do_action('woocommerce_checkout_before_terms_and_conditions') }}
|
||||||
|
|
||||||
{% if wc_terms_and_conditions_checkbox_enabled() is defined and wc_terms_and_conditions_checkbox_enabled() %}
|
{% if wc_terms_and_conditions_checkbox_enabled() %}
|
||||||
<div class="woocommerce-terms-and-conditions-wrapper mb-3">
|
<div class="woocommerce-terms-and-conditions-wrapper mb-3">
|
||||||
{{ do_action('woocommerce_checkout_terms_and_conditions') }}
|
{{ do_action('woocommerce_checkout_terms_and_conditions') }}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{# Previous button #}
|
{# Previous button #}
|
||||||
<li class="page-item{% if current_page <= 1 %} disabled{% endif %}">
|
<li class="page-item{% if current_page <= 1 %} disabled{% endif %}">
|
||||||
<a class="page-link" href="?page={{ current_page - 1 }}" aria-label="{{ __('Previous') }}">
|
<a class="page-link" href="{{ fn('get_pagenum_link', current_page > 1 ? current_page - 1 : 1)|esc_url }}" aria-label="{{ __('Previous') }}">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% elseif i == 1 or i == max_pages or (i >= current_page - 2 and i <= current_page + 2) %}
|
{% elseif i == 1 or i == max_pages or (i >= current_page - 2 and i <= current_page + 2) %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
|
<a class="page-link" href="{{ fn('get_pagenum_link', i)|esc_url }}">{{ i }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% elseif i == current_page - 3 or i == current_page + 3 %}
|
{% elseif i == current_page - 3 or i == current_page + 3 %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
{# Next button #}
|
{# Next button #}
|
||||||
<li class="page-item{% if current_page >= max_pages %} disabled{% endif %}">
|
<li class="page-item{% if current_page >= max_pages %} disabled{% endif %}">
|
||||||
<a class="page-link" href="?page={{ current_page + 1 }}" aria-label="{{ __('Next') }}">
|
<a class="page-link" href="{{ fn('get_pagenum_link', current_page + 1)|esc_url }}" aria-label="{{ __('Next') }}">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
# Product Content in Loop (Bootstrap 5 Override)
|
# Product Content in Loop (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Renders a single product card within the shop loop grid.
|
# Renders a single product card within the shop loop grid.
|
||||||
# Uses Bootstrap 5 card component with stretched-link.
|
# Uses Bootstrap 5 card component with WooCommerce hook output.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Rendered via the content-product.php bridge file (not TemplateOverride)
|
||||||
# product - WC_Product object with:
|
# because wc_get_template_part() does not fire the template_part hooks.
|
||||||
# .name - Product name
|
#
|
||||||
# .permalink - Product URL
|
# Hook output structure:
|
||||||
# .image - Product thumbnail HTML
|
# woocommerce_before_shop_loop_item → <a> link open
|
||||||
# .price_html - Formatted price HTML
|
# woocommerce_before_shop_loop_item_title → sale badge, product image
|
||||||
# .rating_html - Star rating HTML
|
# woocommerce_shop_loop_item_title → <h2> product title
|
||||||
# .is_on_sale - Whether product is on sale
|
# woocommerce_after_shop_loop_item_title → star rating, price
|
||||||
# .add_to_cart - Add-to-cart button context
|
# woocommerce_after_shop_loop_item → </a> link close, add-to-cart button
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: content-product.php
|
# WooCommerce PHP equivalent: content-product.php
|
||||||
#
|
#
|
||||||
|
|||||||
56
templates/content-single-product.html.twig
Normal file
56
templates/content-single-product.html.twig
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{#
|
||||||
|
# Single Product Content (Bootstrap 5 Override)
|
||||||
|
#
|
||||||
|
# Renders the single product page with a Bootstrap 5 two-column grid:
|
||||||
|
# Left column (col-lg-6): Product images (sale flash + gallery)
|
||||||
|
# Right column (col-lg-6): Product summary (title, rating, price, excerpt,
|
||||||
|
# add-to-cart, meta, sharing)
|
||||||
|
# Full-width rows below: Tabs, upsells, related products
|
||||||
|
#
|
||||||
|
# All individual components are rendered via WooCommerce action hooks,
|
||||||
|
# which trigger the Bootstrap 5 sub-templates through TemplateOverride.
|
||||||
|
#
|
||||||
|
# Rendered via the content-single-product.php bridge file (not TemplateOverride)
|
||||||
|
# because wc_get_template_part() does not fire the template_part hooks.
|
||||||
|
#
|
||||||
|
# Hook output structure:
|
||||||
|
# woocommerce_before_single_product_summary → sale flash (10), product images (20)
|
||||||
|
# woocommerce_single_product_summary → title (5), rating (10), price (10),
|
||||||
|
# excerpt (20), add-to-cart (30),
|
||||||
|
# meta (40), sharing (50)
|
||||||
|
# woocommerce_after_single_product_summary → tabs (10), upsells (15), related (20)
|
||||||
|
#
|
||||||
|
# Context (from bridge file):
|
||||||
|
# product - WC_Product object
|
||||||
|
# product_id - Product post ID
|
||||||
|
# product_class - Space-separated CSS class string from wc_get_product_class()
|
||||||
|
#
|
||||||
|
# WooCommerce PHP equivalent: content-single-product.php
|
||||||
|
#
|
||||||
|
# @package WcBootstrap
|
||||||
|
# @since 0.1.0
|
||||||
|
#}
|
||||||
|
|
||||||
|
<div id="product-{{ product_id }}" class="{{ product_class }}">
|
||||||
|
|
||||||
|
{# Two-column layout: images left, summary right #}
|
||||||
|
<div class="row gx-4 gx-lg-5 mb-5">
|
||||||
|
{# Left column: Sale flash + Product images #}
|
||||||
|
<div class="col-lg-6 position-relative">
|
||||||
|
{{ do_action('woocommerce_before_single_product_summary') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Right column: Product summary #}
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="summary entry-summary">
|
||||||
|
{{ do_action('woocommerce_single_product_summary') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Full-width sections: Tabs, Upsells, Related Products #}
|
||||||
|
{{ do_action('woocommerce_after_single_product_summary') }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ do_action('woocommerce_after_single_product') }}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Shop Breadcrumb (Bootstrap 5 Override)
|
# Shop Breadcrumb (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Replaces WooCommerce's breadcrumb with Bootstrap 5 breadcrumb component.
|
# Replaces WooCommerce's breadcrumb with Bootstrap 5 breadcrumb component.
|
||||||
# Skipped when parent theme is wrapping (base.html.twig handles breadcrumbs).
|
# Skipped when parent theme is wrapping (wc-base.html.twig handles breadcrumbs).
|
||||||
#
|
#
|
||||||
# Expected context (from WooCommerce woocommerce_breadcrumb()):
|
# Expected context (from WooCommerce woocommerce_breadcrumb()):
|
||||||
# breadcrumb - Array of [label, url] tuples
|
# breadcrumb - Array of [label, url] tuples
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
{% if not is_user_logged_in() %}
|
{% if not is_user_logged_in() %}
|
||||||
<form class="woocommerce-form woocommerce-form-login login{% if hidden %} d-none{% endif %}" method="post">
|
<form class="woocommerce-form woocommerce-form-login login{% if hidden is defined and hidden %} d-none{% endif %}" method="post">
|
||||||
|
|
||||||
{{ do_action('woocommerce_login_form_start') }}
|
{{ do_action('woocommerce_login_form_start') }}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Replaces WooCommerce's theme-specific wrapper divs with Bootstrap 5 layout.
|
# Replaces WooCommerce's theme-specific wrapper divs with Bootstrap 5 layout.
|
||||||
# When the parent theme wraps the page (_theme_wrapped), this outputs nothing
|
# When the parent theme wraps the page (_theme_wrapped), this outputs nothing
|
||||||
# since base.html.twig already provides the container.
|
# since wc-base.html.twig already provides the container.
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: global/wrapper-start.php
|
# WooCommerce PHP equivalent: global/wrapper-start.php
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends "base.html.twig" %}
|
{% extends "wc-base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
<article>
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% set cols = columns|default(3) %}
|
{% set cols = columns|default(4) %}
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-{{ cols < 3 ? cols : 3 }} row-cols-lg-{{ cols }} g-4 products">
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-{{ cols }} g-4 products">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
{% if total <= per_page|default(total) or total == 0 %}
|
{% if total <= per_page|default(total) or total == 0 %}
|
||||||
{{ _n('Showing the single result', 'Showing all %d results', total)|format(total) }}
|
{{ _n('Showing the single result', 'Showing all %d results', total)|format(total) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ __('Showing %1$d–%2$d of %3$d results')|format(first, last, total) }}
|
{{ __('Showing %1$d–%2$d of %3$d results')|format(first, last, total)|raw }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# My Account Dashboard (Bootstrap 5 Override)
|
# My Account Dashboard (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Shows the welcome screen on the account dashboard.
|
# Shows a card-based dashboard with welcome greeting and
|
||||||
|
# quick action links to each account section.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# current_user - WP_User object
|
# current_user - WP_User object
|
||||||
@@ -12,29 +13,53 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
<p>
|
{% set quick_actions = [
|
||||||
{{ __('Hello %1$s (not %1$s? <a href="%2$s">Log out</a>)')|format(
|
{ endpoint: 'orders', icon: 'bi-bag', label: __('Orders'), desc: __('View and track your orders') },
|
||||||
'<strong>' ~ current_user.display_name|esc_html ~ '</strong>',
|
{ endpoint: 'edit-address', icon: 'bi-geo-alt', label: __('Addresses'), desc: __('Manage billing & shipping') },
|
||||||
wc_logout_url()|esc_url
|
{ endpoint: 'edit-account', icon: 'bi-person-gear', label: __('Account details'), desc: __('Update name, email & password') },
|
||||||
)|wp_kses_post }}
|
{ endpoint: 'downloads', icon: 'bi-download', label: __('Downloads'), desc: __('Access purchased files') },
|
||||||
</p>
|
{ endpoint: 'payment-methods', icon: 'bi-credit-card', label: __('Payment methods'), desc: __('Manage saved cards') },
|
||||||
|
] %}
|
||||||
|
|
||||||
<p>
|
<div class="card shadow-sm mb-4">
|
||||||
{% if wc_shipping_enabled() %}
|
<div class="card-body d-flex align-items-center gap-3">
|
||||||
{{ __('From your account dashboard you can view your <a href="%1$s">recent orders</a>, manage your <a href="%2$s">shipping and billing addresses</a>, and <a href="%3$s">edit your password and account details</a>.')|format(
|
<div class="flex-shrink-0">
|
||||||
wc_get_endpoint_url('orders')|esc_url,
|
{{ get_avatar(current_user.ID, 64)|raw }}
|
||||||
wc_get_endpoint_url('edit-address')|esc_url,
|
</div>
|
||||||
wc_get_endpoint_url('edit-account')|esc_url
|
<div class="flex-grow-1">
|
||||||
)|wp_kses_post }}
|
<h2 class="h5 mb-1">
|
||||||
{% else %}
|
{{ __('Hello, %s!')|format(current_user.display_name|esc_html) }}
|
||||||
{{ __('From your account dashboard you can view your <a href="%1$s">recent orders</a>, manage your <a href="%2$s">billing address</a>, and <a href="%3$s">edit your password and account details</a>.')|format(
|
</h2>
|
||||||
wc_get_endpoint_url('orders')|esc_url,
|
<p class="text-body-secondary mb-0">
|
||||||
wc_get_endpoint_url('edit-address')|esc_url,
|
{{ __('Not %s?')|format(current_user.display_name|esc_html) }}
|
||||||
wc_get_endpoint_url('edit-account')|esc_url
|
<a href="{{ wc_logout_url()|esc_url }}">{{ __('Log out') }}</a>
|
||||||
)|wp_kses_post }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3 mb-4">
|
||||||
|
{% for action in quick_actions %}
|
||||||
|
<div class="col">
|
||||||
|
<a href="{{ wc_get_endpoint_url(action.endpoint)|esc_url }}"
|
||||||
|
class="card h-100 text-decoration-none shadow-sm">
|
||||||
|
<div class="card-body d-flex align-items-start gap-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="d-inline-flex align-items-center justify-content-center
|
||||||
|
bg-primary-subtle text-primary rounded-3"
|
||||||
|
style="width: 3rem; height: 3rem;"
|
||||||
|
aria-hidden="true">
|
||||||
|
<i class="bi {{ action.icon }} fs-4"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 mb-1 text-body">{{ action.label }}</h3>
|
||||||
|
<p class="text-body-secondary small mb-0">{{ action.desc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ do_action('woocommerce_account_dashboard') }}
|
{{ do_action('woocommerce_account_dashboard') }}
|
||||||
{{ do_action('woocommerce_before_my_account') }}
|
|
||||||
{{ do_action('woocommerce_after_my_account') }}
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# Edit Account Form (Bootstrap 5 Override)
|
# Edit Account Form (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Account details editing form with Bootstrap form styling.
|
# Account details editing form with card-based sections
|
||||||
|
# for personal information and password change.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# user - WP_User object
|
# user - WP_User object
|
||||||
@@ -18,6 +19,14 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_edit_account_form_start') }}
|
{{ do_action('woocommerce_edit_account_form_start') }}
|
||||||
|
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<i class="bi bi-person me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Personal information') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<label for="account_first_name" class="form-label">
|
<label for="account_first_name" class="form-label">
|
||||||
@@ -67,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-0">
|
||||||
<label for="account_email" class="form-label">
|
<label for="account_email" class="form-label">
|
||||||
{{ __('Email address') }} <span class="text-danger" aria-hidden="true">*</span>
|
{{ __('Email address') }} <span class="text-danger" aria-hidden="true">*</span>
|
||||||
<span class="visually-hidden">{{ __('Required') }}</span>
|
<span class="visually-hidden">{{ __('Required') }}</span>
|
||||||
@@ -83,10 +92,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ do_action('woocommerce_edit_account_form_fields') }}
|
{{ do_action('woocommerce_edit_account_form_fields') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset class="mb-3">
|
<div class="card shadow-sm mb-4">
|
||||||
<legend class="h5 border-bottom pb-2 mb-3">{{ __('Password change') }}</legend>
|
<div class="card-header">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<i class="bi bi-shield-lock me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Password change') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password_current" class="form-label">
|
<label for="password_current" class="form-label">
|
||||||
{{ __('Current password (leave blank to leave unchanged)') }}
|
{{ __('Current password (leave blank to leave unchanged)') }}
|
||||||
@@ -109,7 +125,7 @@
|
|||||||
autocomplete="off" />
|
autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-0">
|
||||||
<label for="password_2" class="form-label">
|
<label for="password_2" class="form-label">
|
||||||
{{ __('Confirm new password') }}
|
{{ __('Confirm new password') }}
|
||||||
</label>
|
</label>
|
||||||
@@ -119,7 +135,8 @@
|
|||||||
id="password_2"
|
id="password_2"
|
||||||
autocomplete="off" />
|
autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ do_action('woocommerce_edit_account_form') }}
|
{{ do_action('woocommerce_edit_account_form') }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# Edit Address Form (Bootstrap 5 Override)
|
# Edit Address Form (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Address editing form with Bootstrap form styling.
|
# Address editing form wrapped in a Bootstrap card with
|
||||||
|
# contextual icon for billing/shipping.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# load_address - Address type ('billing' or 'shipping'), or empty
|
# load_address - Address type ('billing' or 'shipping'), or empty
|
||||||
@@ -14,15 +15,22 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
{% set page_title = load_address == 'billing' ? __('Billing address') : __('Shipping address') %}
|
{% set page_title = load_address == 'billing' ? __('Billing address') : __('Shipping address') %}
|
||||||
|
{% set address_icon = load_address == 'billing' ? 'bi-receipt' : 'bi-truck' %}
|
||||||
|
|
||||||
{{ do_action('woocommerce_before_edit_account_address_form') }}
|
{{ do_action('woocommerce_before_edit_account_address_form') }}
|
||||||
|
|
||||||
{% if not load_address %}
|
{% if not load_address %}
|
||||||
{% include 'myaccount/my-address.html.twig' %}
|
{% include 'myaccount/my-address.html.twig' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<i class="bi {{ address_icon }} me-1" aria-hidden="true"></i>
|
||||||
|
{{ apply_filters('woocommerce_my_account_edit_address_title', page_title, load_address) }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<form method="post" novalidate>
|
<form method="post" novalidate>
|
||||||
<h2 class="h4 mb-4">{{ apply_filters('woocommerce_my_account_edit_address_title', page_title, load_address) }}</h2>
|
|
||||||
|
|
||||||
<div class="woocommerce-address-fields">
|
<div class="woocommerce-address-fields">
|
||||||
{{ do_action('woocommerce_before_edit_address_form_' ~ load_address) }}
|
{{ do_action('woocommerce_before_edit_address_form_' ~ load_address) }}
|
||||||
|
|
||||||
@@ -44,6 +52,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ do_action('woocommerce_after_edit_account_address_form') }}
|
{{ do_action('woocommerce_after_edit_account_address_form') }}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# Lost Password Form (Bootstrap 5 Override)
|
# Lost Password Form (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Form to request a password reset email.
|
# Form to request a password reset email, wrapped in a
|
||||||
|
# Bootstrap card for visual consistency.
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: myaccount/form-lost-password.php
|
# WooCommerce PHP equivalent: myaccount/form-lost-password.php
|
||||||
#
|
#
|
||||||
@@ -11,9 +12,17 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_before_lost_password_form') }}
|
{{ do_action('woocommerce_before_lost_password_form') }}
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<i class="bi bi-key me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Lost your password?') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<form method="post" class="woocommerce-ResetPassword lost_reset_password">
|
<form method="post" class="woocommerce-ResetPassword lost_reset_password">
|
||||||
|
|
||||||
<p class="mb-3">
|
<p class="text-body-secondary mb-3">
|
||||||
{{ apply_filters('woocommerce_lost_password_message', __('Lost your password? Please enter your username or email address. You will receive a link to create a new password via email.')) }}
|
{{ apply_filters('woocommerce_lost_password_message', __('Lost your password? Please enter your username or email address. You will receive a link to create a new password via email.')) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -43,5 +52,7 @@
|
|||||||
{{ wp_nonce_field('lost_password', 'woocommerce-lost-password-nonce') }}
|
{{ wp_nonce_field('lost_password', 'woocommerce-lost-password-nonce') }}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ do_action('woocommerce_after_lost_password_form') }}
|
{{ do_action('woocommerce_after_lost_password_form') }}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# Reset Password Form (Bootstrap 5 Override)
|
# Reset Password Form (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Form to set a new password after clicking the reset link.
|
# Form to set a new password after clicking the reset link,
|
||||||
|
# wrapped in a Bootstrap card for visual consistency.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# args - Array with 'key' and 'login' values
|
# args - Array with 'key' and 'login' values
|
||||||
@@ -14,9 +15,17 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_before_reset_password_form') }}
|
{{ do_action('woocommerce_before_reset_password_form') }}
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<i class="bi bi-shield-lock me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Set new password') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<form method="post" class="woocommerce-ResetPassword lost_reset_password">
|
<form method="post" class="woocommerce-ResetPassword lost_reset_password">
|
||||||
|
|
||||||
<p class="mb-3">
|
<p class="text-body-secondary mb-3">
|
||||||
{{ apply_filters('woocommerce_reset_password_message', __('Enter a new password below.')) }}
|
{{ apply_filters('woocommerce_reset_password_message', __('Enter a new password below.')) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -64,5 +73,7 @@
|
|||||||
{{ wp_nonce_field('reset_password', 'woocommerce-reset-password-nonce') }}
|
{{ wp_nonce_field('reset_password', 'woocommerce-reset-password-nonce') }}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ do_action('woocommerce_after_reset_password_form') }}
|
{{ do_action('woocommerce_after_reset_password_form') }}
|
||||||
|
|||||||
@@ -16,8 +16,13 @@
|
|||||||
|
|
||||||
{{ do_action('woocommerce_before_lost_password_confirmation_message') }}
|
{{ do_action('woocommerce_before_lost_password_confirmation_message') }}
|
||||||
|
|
||||||
<p>
|
<p class="text-body-secondary">
|
||||||
{{ apply_filters('woocommerce_lost_password_confirmation_message', __('A password reset email has been sent to the email address on file for your account, but may take several minutes to show up in your inbox. Please wait at least 10 minutes before attempting another reset.')) }}
|
{{ apply_filters('woocommerce_lost_password_confirmation_message', __('A password reset email has been sent to the email address on file for your account, but may take several minutes to show up in your inbox. Please wait at least 10 minutes before attempting another reset.')) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<a href="{{ wc_get_page_permalink('myaccount')|esc_url }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Back to login') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
{{ do_action('woocommerce_after_lost_password_confirmation_message') }}
|
{{ do_action('woocommerce_after_lost_password_confirmation_message') }}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
{{ do_action('woocommerce_before_my_account') }}
|
{{ do_action('woocommerce_before_my_account') }}
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-auto">
|
||||||
{% include 'myaccount/navigation.html.twig' %}
|
{% include 'myaccount/navigation.html.twig' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-9">
|
<div class="col-lg">
|
||||||
<div class="woocommerce-MyAccount-content">
|
<div class="woocommerce-MyAccount-content">
|
||||||
{{ do_action('woocommerce_account_content') }}
|
{{ do_action('woocommerce_account_content') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# My Account Navigation (Bootstrap 5 Override)
|
# My Account Navigation (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Renders the sidebar navigation for the My Account area
|
# Renders the sidebar navigation for the My Account area
|
||||||
# using Bootstrap list-group component.
|
# using Bootstrap list-group component with icons.
|
||||||
|
# Responsive: offcanvas on mobile, sticky sidebar on desktop.
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: myaccount/navigation.php
|
# WooCommerce PHP equivalent: myaccount/navigation.php
|
||||||
#
|
#
|
||||||
@@ -10,14 +11,53 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
<nav class="woocommerce-MyAccount-navigation" aria-label="{{ __('Account navigation') }}">
|
{% set endpoint_icons = {
|
||||||
|
'dashboard': 'bi-speedometer2',
|
||||||
|
'orders': 'bi-bag',
|
||||||
|
'downloads': 'bi-download',
|
||||||
|
'edit-address': 'bi-geo-alt',
|
||||||
|
'payment-methods': 'bi-credit-card',
|
||||||
|
'edit-account': 'bi-person-gear',
|
||||||
|
'customer-logout': 'bi-box-arrow-right',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{{ do_action('woocommerce_before_account_navigation') }}
|
||||||
|
|
||||||
|
<button class="btn btn-outline-secondary w-100 mb-3 d-lg-none"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#accountNav"
|
||||||
|
aria-controls="accountNav">
|
||||||
|
<i class="bi bi-list me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Account menu') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="offcanvas-lg offcanvas-start" id="accountNav" tabindex="-1"
|
||||||
|
aria-labelledby="accountNavLabel">
|
||||||
|
<div class="offcanvas-header d-lg-none">
|
||||||
|
<h5 class="offcanvas-title" id="accountNavLabel">{{ __('Account menu') }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"
|
||||||
|
data-bs-target="#accountNav" aria-label="{{ __('Close') }}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body p-0">
|
||||||
|
<nav class="woocommerce-MyAccount-navigation position-sticky"
|
||||||
|
style="top: 5rem;"
|
||||||
|
aria-label="{{ __('Account navigation') }}">
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for endpoint, label in wc_get_account_menu_items() %}
|
{% for endpoint, label in wc_get_account_menu_items() %}
|
||||||
|
{% set is_active = wc_get_account_menu_item_classes(endpoint) matches '/is-active/' %}
|
||||||
<a href="{{ wc_get_account_endpoint_url(endpoint)|esc_url }}"
|
<a href="{{ wc_get_account_endpoint_url(endpoint)|esc_url }}"
|
||||||
class="list-group-item list-group-item-action{% if wc_get_account_menu_item_classes(endpoint) matches '/is-active/' %} active{% endif %}"
|
class="list-group-item list-group-item-action d-flex align-items-center{% if is_active %} active{% endif %}"
|
||||||
{% if wc_get_account_menu_item_classes(endpoint) matches '/is-active/' %}aria-current="page"{% endif %}>
|
{% if is_active %}aria-current="page"{% endif %}>
|
||||||
|
{% if endpoint_icons[endpoint] is defined %}
|
||||||
|
<i class="bi {{ endpoint_icons[endpoint] }} me-2" aria-hidden="true"></i>
|
||||||
|
{% endif %}
|
||||||
{{ label|esc_html }}
|
{{ label|esc_html }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ do_action('woocommerce_after_account_navigation') }}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# View Order (Bootstrap 5 Override)
|
# View Order (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Shows details of a specific order on the account page.
|
# Shows details of a specific order on the account page
|
||||||
|
# with summary card, status badge, and order notes.
|
||||||
# HPOS compatible: uses WC_Order methods only.
|
# HPOS compatible: uses WC_Order methods only.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
@@ -16,21 +17,51 @@
|
|||||||
|
|
||||||
{% set notes = order.get_customer_order_notes() %}
|
{% set notes = order.get_customer_order_notes() %}
|
||||||
|
|
||||||
<p>
|
<div class="card shadow-sm mb-4">
|
||||||
{{ __('Order #%1$s was placed on %2$s and is currently %3$s.')|format(
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
'<mark class="order-number">' ~ order.get_order_number() ~ '</mark>',
|
<h2 class="h5 mb-0">
|
||||||
'<mark class="order-date">' ~ wc_format_datetime(order.get_date_created()) ~ '</mark>',
|
{{ __('Order #%s')|format(order.get_order_number()) }}
|
||||||
'<mark class="order-status">' ~ wc_get_order_status_name(order.get_status()) ~ '</mark>'
|
</h2>
|
||||||
)|wp_kses_post }}
|
{% include 'components/status-badge.html.twig' with { status: order.get_status() } %}
|
||||||
</p>
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between">
|
||||||
|
<span class="text-body-secondary">{{ __('Date') }}</span>
|
||||||
|
<time datetime="{{ order.get_date_created().date('c')|esc_attr }}">
|
||||||
|
{{ wc_format_datetime(order.get_date_created())|esc_html }}
|
||||||
|
</time>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between">
|
||||||
|
<span class="text-body-secondary">{{ __('Status') }}</span>
|
||||||
|
{% include 'components/status-badge.html.twig' with { status: order.get_status() } %}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between">
|
||||||
|
<span class="text-body-secondary">{{ __('Total') }}</span>
|
||||||
|
<strong>{{ order.get_formatted_order_total()|wp_kses_post }}</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>
|
||||||
|
<span>{{ order.get_payment_method_title()|esc_html }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if notes %}
|
{% if notes %}
|
||||||
<h2 class="h5 mt-4 mb-3">{{ __('Order updates') }}</h2>
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="list-group mb-4">
|
<div class="card-header">
|
||||||
|
<h3 class="h6 mb-0">
|
||||||
|
<i class="bi bi-chat-left-text me-1" aria-hidden="true"></i>
|
||||||
|
{{ __('Order updates') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
{% for note in notes %}
|
{% for note in notes %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<small class="text-body-secondary">
|
<small class="text-body-secondary">
|
||||||
|
<i class="bi bi-clock me-1" aria-hidden="true"></i>
|
||||||
{{ date_i18n(__('l jS \\o\\f F Y, h:ia'), strtotime(note.comment_date)) }}
|
{{ date_i18n(__('l jS \\o\\f F Y, h:ia'), strtotime(note.comment_date)) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,6 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ do_action('woocommerce_view_order', order_id) }}
|
{{ do_action('woocommerce_view_order', order_id) }}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
{% if order.get_customer_note() %}
|
{% if order.get_customer_note() %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ __('Note:') }}</th>
|
<th>{{ __('Note:') }}</th>
|
||||||
<td class="text-end">{{ order.get_customer_note()|nl2br|esc_html }}</td>
|
<td class="text-end">{{ order.get_customer_note()|esc_html|nl2br }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<input type="search"
|
<input type="search"
|
||||||
id="{{ field_id }}"
|
id="{{ field_id }}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="{{ __('Search products…') }}"
|
placeholder="{{ __('Search products…') }}"
|
||||||
value="{{ get_search_query() }}"
|
value="{{ get_search_query() }}"
|
||||||
name="s" />
|
name="s" />
|
||||||
<button type="submit" class="btn btn-outline-primary" aria-label="{{ __('Search') }}">
|
<button type="submit" class="btn btn-outline-primary" aria-label="{{ __('Search') }}">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
{% verbatim %}
|
||||||
<script type="text/template" id="tmpl-variation-template">
|
<script type="text/template" id="tmpl-variation-template">
|
||||||
<div class="woocommerce-variation-description mb-2">
|
<div class="woocommerce-variation-description mb-2">
|
||||||
{{{ data.variation.variation_description }}}
|
{{{ data.variation.variation_description }}}
|
||||||
@@ -22,9 +23,10 @@
|
|||||||
{{{ data.variation.availability_html }}}
|
{{{ data.variation.availability_html }}}
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
{% endverbatim %}
|
||||||
|
|
||||||
<script type="text/template" id="tmpl-unavailable-variation-template">
|
<script type="text/template" id="tmpl-unavailable-variation-template">
|
||||||
<p class="alert alert-warning mb-0">
|
<p class="alert alert-warning mb-0" role="alert">
|
||||||
{{ __('Sorry, this product is unavailable. Please choose a different combination.') }}
|
{{ __('Sorry, this product is unavailable. Please choose a different combination.') }}
|
||||||
</p>
|
</p>
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,12 +4,7 @@
|
|||||||
# Renders SKU, categories, and tags as a definition list.
|
# Renders SKU, categories, and tags as a definition list.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# product - WC_Product object with:
|
# product - WC_Product object (from TemplateOverride)
|
||||||
# .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
|
# WooCommerce PHP equivalent: single-product/meta.php
|
||||||
#
|
#
|
||||||
@@ -17,6 +12,14 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
{# Compute categories/tags HTML when not passed as context. #}
|
||||||
|
{% if categories_html is not defined %}
|
||||||
|
{% set categories_html = fn('wc_get_product_category_list', product.get_id(), ', ') %}
|
||||||
|
{% endif %}
|
||||||
|
{% if tags_html is not defined %}
|
||||||
|
{% set tags_html = fn('wc_get_product_tag_list', product.get_id(), ', ') %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="product_meta border-top pt-3 mt-3">
|
<div class="product_meta border-top pt-3 mt-3">
|
||||||
{{ do_action('woocommerce_product_meta_start') }}
|
{{ do_action('woocommerce_product_meta_start') }}
|
||||||
|
|
||||||
@@ -26,13 +29,13 @@
|
|||||||
<dd class="col-sm-9">{{ product.get_sku()|esc_html }}</dd>
|
<dd class="col-sm-9">{{ product.get_sku()|esc_html }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if categories_html is defined and categories_html %}
|
{% if categories_html %}
|
||||||
<dt class="col-sm-3 text-body-secondary">{{ __('Categories:') }}</dt>
|
<dt class="col-sm-3 text-body-secondary">{{ fn('_n', 'Category:', 'Categories:', product.get_category_ids()|length, 'woocommerce') }}</dt>
|
||||||
<dd class="col-sm-9">{{ categories_html|raw }}</dd>
|
<dd class="col-sm-9">{{ categories_html|raw }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if tags_html is defined and tags_html %}
|
{% if tags_html %}
|
||||||
<dt class="col-sm-3 text-body-secondary">{{ __('Tags:') }}</dt>
|
<dt class="col-sm-3 text-body-secondary">{{ fn('_n', 'Tag:', 'Tags:', product.get_tag_ids()|length, 'woocommerce') }}</dt>
|
||||||
<dd class="col-sm-9">{{ tags_html|raw }}</dd>
|
<dd class="col-sm-9">{{ tags_html|raw }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -16,9 +16,16 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% set gallery_classes = 'woocommerce-product-gallery' %}
|
{% set cols = columns|default(4) %}
|
||||||
|
{% set has_images = post_thumbnail_id is defined and post_thumbnail_id %}
|
||||||
|
{% set gallery_classes = 'woocommerce-product-gallery woocommerce-product-gallery--columns-' ~ cols %}
|
||||||
|
{% if has_images %}
|
||||||
|
{% set gallery_classes = gallery_classes ~ ' woocommerce-product-gallery--with-images' %}
|
||||||
|
{% else %}
|
||||||
|
{% set gallery_classes = gallery_classes ~ ' woocommerce-product-gallery--without-images' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="{{ gallery_classes }}" data-columns="{{ columns|default(4) }}">
|
<div class="{{ gallery_classes }}" data-columns="{{ cols }}" style="opacity: 0; transition: opacity .25s ease-in-out;">
|
||||||
<div class="woocommerce-product-gallery__wrapper">
|
<div class="woocommerce-product-gallery__wrapper">
|
||||||
{# Main product image #}
|
{# Main product image #}
|
||||||
{% if main_image_html is defined and main_image_html %}
|
{% if main_image_html is defined and main_image_html %}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
# Renders the star rating with review count link on the single product page.
|
# Renders the star rating with review count link on the single product page.
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# product - WC_Product object
|
# product - WC_Product object (from TemplateOverride)
|
||||||
# rating_count - Number of ratings
|
|
||||||
# review_count - Number of reviews
|
|
||||||
# average - Average rating (0-5)
|
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: single-product/rating.php
|
# WooCommerce PHP equivalent: single-product/rating.php
|
||||||
#
|
#
|
||||||
@@ -15,7 +12,17 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% if rating_count is defined and rating_count > 0 %}
|
{# Guard: bail if ratings are disabled. #}
|
||||||
|
{% if fn('wc_review_ratings_enabled') %}
|
||||||
|
|
||||||
|
{# Compute rating data from product object when not passed as context. #}
|
||||||
|
{% if rating_count is not defined %}
|
||||||
|
{% set rating_count = product.get_rating_count() %}
|
||||||
|
{% set review_count = product.get_review_count() %}
|
||||||
|
{% set average = product.get_average_rating() %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if rating_count > 0 %}
|
||||||
<div class="woocommerce-product-rating d-flex align-items-center gap-2 mb-3">
|
<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"
|
<div class="wc-star-rating d-flex align-items-center gap-1"
|
||||||
role="img"
|
role="img"
|
||||||
@@ -31,10 +38,12 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if review_count is defined and review_count > 0 %}
|
{% if review_count > 0 %}
|
||||||
<a href="#reviews" class="woocommerce-review-link text-body-secondary text-decoration-none small" rel="nofollow">
|
<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) }}
|
{{ fn('_n', '%s customer review', '%s customer reviews', review_count, 'woocommerce')|format(review_count) }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}{# wc_review_ratings_enabled #}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<section class="related products mt-5">
|
<section class="related products mt-5">
|
||||||
{% set heading = heading|default(__('Related products')) %}
|
{% set heading = heading|default(__('Related products')) %}
|
||||||
{% if heading %}
|
{% if heading %}
|
||||||
<h2 class="h4 mb-4">{{ heading|esc_html }}</h2>
|
<h2 class="h4 mb-4">{{ heading|raw }}</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ woocommerce_product_loop_start() }}
|
{{ woocommerce_product_loop_start() }}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# Product Short Description (Bootstrap 5 Override)
|
# Product Short Description (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Expected context:
|
# Expected context:
|
||||||
# short_description - Product short description HTML
|
# product - WC_Product object (from TemplateOverride)
|
||||||
|
# short_description - Product short description HTML (optional)
|
||||||
#
|
#
|
||||||
# WooCommerce PHP equivalent: single-product/short-description.php
|
# WooCommerce PHP equivalent: single-product/short-description.php
|
||||||
#
|
#
|
||||||
@@ -10,7 +11,12 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% if short_description is defined and short_description %}
|
{# Compute short description when not passed as context. #}
|
||||||
|
{% if short_description is not defined %}
|
||||||
|
{% set short_description = apply_filters('woocommerce_short_description', product.get_short_description()) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if short_description %}
|
||||||
<div class="woocommerce-product-details__short-description lead text-body-secondary mb-3">
|
<div class="woocommerce-product-details__short-description lead text-body-secondary mb-3">
|
||||||
{{ short_description|raw }}
|
{{ short_description|raw }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
# @since 0.1.0
|
# @since 0.1.0
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% if product_tabs is defined and product_tabs|length > 0 %}
|
{# Compute tabs from filter when not passed as context (wc_get_template passes no args). #}
|
||||||
|
{% if product_tabs is not defined %}
|
||||||
|
{% set product_tabs = apply_filters('woocommerce_product_tabs', {}) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product_tabs|length > 0 %}
|
||||||
<div class="woocommerce-tabs wc-tabs-wrapper mt-5">
|
<div class="woocommerce-tabs wc-tabs-wrapper mt-5">
|
||||||
{# Tab navigation #}
|
{# Tab navigation #}
|
||||||
<ul class="nav nav-tabs" id="productTabs" role="tablist">
|
<ul class="nav nav-tabs" id="productTabs" role="tablist">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<section class="up-sells upsells products mt-5">
|
<section class="up-sells upsells products mt-5">
|
||||||
{% set heading = heading|default(__('You may also like…')) %}
|
{% set heading = heading|default(__('You may also like…')) %}
|
||||||
{% if heading %}
|
{% if heading %}
|
||||||
<h2 class="h4 mb-4">{{ heading }}</h2>
|
<h2 class="h4 mb-4">{{ heading|raw }}</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ woocommerce_product_loop_start() }}
|
{{ woocommerce_product_loop_start() }}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{#
|
{#
|
||||||
# Base Template (Bootstrap 5 Override)
|
# Base Template (Bootstrap 5 Override)
|
||||||
#
|
#
|
||||||
# Overrides the plugin's base.html.twig with Bootstrap 5 components.
|
# WooCommerce base layout (renamed from base.html.twig to avoid
|
||||||
|
# shadowing the parent theme's full page shell).
|
||||||
# Provides the basic structure and block definitions.
|
# Provides the basic structure and block definitions.
|
||||||
#
|
#
|
||||||
# When _theme_wrapped is true, the parent theme already provides the page
|
# When _theme_wrapped is true, the parent theme already provides the page
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
{% block notifications %}
|
{% block notifications %}
|
||||||
{% if notifications is defined and notifications|length > 0 %}
|
{% if notifications is defined and notifications|length > 0 %}
|
||||||
{% for notification in notifications %}
|
{% for notification in notifications %}
|
||||||
<div class="alert alert-{{ notification.type|default('info') }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ notification.type|default('info')|esc_attr }} alert-dismissible fade show" role="alert">
|
||||||
{{ notification.message|wp_kses_post }}
|
{{ notification.message|wp_kses_post }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
{% if flash_messages is defined %}
|
{% if flash_messages is defined %}
|
||||||
{% for type, messages in flash_messages %}
|
{% for type, messages in flash_messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ type }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ type|esc_attr }} alert-dismissible fade show" role="alert">
|
||||||
{{ message|wp_kses_post }}
|
{{ message|wp_kses_post }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
|
||||||
</div>
|
</div>
|
||||||
104
woocommerce/archive-product.php
Normal file
104
woocommerce/archive-product.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Product Archive / Shop Page (Bootstrap 5 Layout)
|
||||||
|
*
|
||||||
|
* Renders the WooCommerce product archive content with a Bootstrap 5 layout
|
||||||
|
* featuring a responsive sidebar (offcanvas on mobile) and product card grid.
|
||||||
|
*
|
||||||
|
* This file is NOT included directly by WordPress. Instead, it is captured via
|
||||||
|
* output buffering by wc_bootstrap_render_product_archive() and injected into
|
||||||
|
* the parent theme's page shell (pages/page.html.twig). Therefore it does NOT
|
||||||
|
* call get_header()/get_footer() or render the wrapper hooks.
|
||||||
|
*
|
||||||
|
* @package WcBootstrap
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
// Fire structured data hook (normally on woocommerce_before_main_content at priority 30).
|
||||||
|
do_action( 'woocommerce_shop_loop_header' );
|
||||||
|
|
||||||
|
$has_sidebar = is_active_sidebar( 'shop-sidebar' );
|
||||||
|
|
||||||
|
if ( woocommerce_product_loop() ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_before_shop_loop.
|
||||||
|
*
|
||||||
|
* @hooked woocommerce_output_all_notices - 10
|
||||||
|
* @hooked woocommerce_result_count - 20
|
||||||
|
* @hooked woocommerce_catalog_ordering - 30
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_before_shop_loop' );
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="row g-4">
|
||||||
|
<?php if ( $has_sidebar ) : ?>
|
||||||
|
<aside class="col-lg-3 mb-4 mb-lg-0">
|
||||||
|
<button class="btn btn-outline-secondary w-100 d-lg-none mb-3"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#shopSidebar"
|
||||||
|
aria-controls="shopSidebar">
|
||||||
|
<i class="bi bi-funnel me-1" aria-hidden="true"></i>
|
||||||
|
<?php esc_html_e( 'Filters', 'wc-bootstrap' ); ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="offcanvas-lg offcanvas-start"
|
||||||
|
id="shopSidebar"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="shopSidebarLabel">
|
||||||
|
<div class="offcanvas-header d-lg-none">
|
||||||
|
<h5 class="offcanvas-title" id="shopSidebarLabel">
|
||||||
|
<?php esc_html_e( 'Filters', 'wc-bootstrap' ); ?>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="<?php esc_attr_e( 'Close', 'wc-bootstrap' ); ?>"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body p-0">
|
||||||
|
<?php dynamic_sidebar( 'shop-sidebar' ); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="col-12">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
woocommerce_product_loop_start();
|
||||||
|
|
||||||
|
if ( wc_get_loop_prop( 'total' ) ) {
|
||||||
|
while ( have_posts() ) {
|
||||||
|
the_post();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_shop_loop.
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_shop_loop' );
|
||||||
|
|
||||||
|
wc_get_template_part( 'content', 'product' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
woocommerce_product_loop_end();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_after_shop_loop.
|
||||||
|
*
|
||||||
|
* @hooked woocommerce_pagination - 10
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_after_shop_loop' );
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_no_products_found.
|
||||||
|
*
|
||||||
|
* @hooked wc_no_products_found - 10
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_no_products_found' );
|
||||||
|
}
|
||||||
33
woocommerce/content-product.php
Normal file
33
woocommerce/content-product.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Product Content in Loop — PHP Bridge to Twig
|
||||||
|
*
|
||||||
|
* Bridge file that renders the Bootstrap 5 card template (content-product.html.twig)
|
||||||
|
* via the parent theme's TwigService instead of WooCommerce's default <li> markup.
|
||||||
|
*
|
||||||
|
* WooCommerce's wc_get_template_part('content', 'product') uses locate_template()
|
||||||
|
* which finds this file in the child theme before falling back to the plugin template.
|
||||||
|
* Unlike wc_get_template(), wc_get_template_part() does NOT fire the
|
||||||
|
* woocommerce_before_template_part / woocommerce_after_template_part hooks,
|
||||||
|
* so the TemplateOverride class cannot intercept it — this bridge file is needed.
|
||||||
|
*
|
||||||
|
* @package WcBootstrap
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// Ensure the product is valid and visible (same guard as WooCommerce's default template).
|
||||||
|
if ( ! is_a( $product, WC_Product::class ) || ! $product->is_visible() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) {
|
||||||
|
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||||
|
echo $twig->render( 'content-product.html.twig', [] );
|
||||||
|
} else {
|
||||||
|
// Fallback: include WooCommerce's default template directly.
|
||||||
|
include WC()->plugin_path() . '/templates/content-product.php';
|
||||||
|
}
|
||||||
46
woocommerce/content-single-product.php
Normal file
46
woocommerce/content-single-product.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Single Product Content — PHP Bridge to Twig
|
||||||
|
*
|
||||||
|
* Bridge file that renders the Bootstrap 5 single product template
|
||||||
|
* (content-single-product.html.twig) via the parent theme's TwigService
|
||||||
|
* instead of WooCommerce's default flat layout.
|
||||||
|
*
|
||||||
|
* WooCommerce's wc_get_template_part('content', 'single-product') uses
|
||||||
|
* locate_template() which finds this file in the child theme before falling
|
||||||
|
* back to the plugin template. Unlike wc_get_template(), wc_get_template_part()
|
||||||
|
* does NOT fire the woocommerce_before_template_part / woocommerce_after_template_part
|
||||||
|
* hooks, so the TemplateOverride class cannot intercept it — this bridge file
|
||||||
|
* is needed.
|
||||||
|
*
|
||||||
|
* @package WcBootstrap
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_before_single_product.
|
||||||
|
*
|
||||||
|
* @hooked woocommerce_output_all_notices - 10
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_before_single_product' );
|
||||||
|
|
||||||
|
if ( post_password_required() ) {
|
||||||
|
echo get_the_password_form(); // WPCS: XSS ok.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) {
|
||||||
|
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||||
|
echo $twig->render( 'content-single-product.html.twig', [
|
||||||
|
'product' => $product,
|
||||||
|
'product_id' => $product->get_id(),
|
||||||
|
'product_class' => implode( ' ', wc_get_product_class( '', $product ) ),
|
||||||
|
] );
|
||||||
|
} else {
|
||||||
|
// Fallback: include WooCommerce's default template directly.
|
||||||
|
include WC()->plugin_path() . '/templates/content-single-product.php';
|
||||||
|
}
|
||||||
35
woocommerce/single-product.php
Normal file
35
woocommerce/single-product.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Single Product Page (Bootstrap 5 Layout)
|
||||||
|
*
|
||||||
|
* Renders the WooCommerce single product content. This file is NOT included
|
||||||
|
* directly by WordPress. Instead, it is captured via output buffering by
|
||||||
|
* wc_bootstrap_render_single_product() and injected into the parent theme's
|
||||||
|
* page shell (pages/page.html.twig). Therefore it does NOT call
|
||||||
|
* get_header()/get_footer() or render the wrapper hooks.
|
||||||
|
*
|
||||||
|
* @package WcBootstrap
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_before_main_content.
|
||||||
|
*
|
||||||
|
* @hooked woocommerce_output_content_wrapper - 10 (outputs opening divs for the content)
|
||||||
|
* @hooked woocommerce_breadcrumb - 20
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_before_main_content' );
|
||||||
|
|
||||||
|
while ( have_posts() ) {
|
||||||
|
the_post();
|
||||||
|
wc_get_template_part( 'content', 'single-product' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: woocommerce_after_main_content.
|
||||||
|
*
|
||||||
|
* @hooked woocommerce_output_content_wrapper_end - 10 (outputs closing divs for the content)
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_after_main_content' );
|
||||||
Reference in New Issue
Block a user