feat: implement WooCommerce storefront foundation
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/* XShop WooCommerce — tokens extension + shop/product/cart/account. Logical props only. */
|
||||
|
||||
/* ---- Woo tokens ---- */
|
||||
:root {
|
||||
--xshop-wc-badge-sale: var(--xshop-danger);
|
||||
--xshop-wc-badge-stock: #6b7280;
|
||||
--xshop-wc-price: var(--xshop-text);
|
||||
--xshop-wc-price-sale: var(--xshop-danger);
|
||||
--xshop-wc-price-regular: var(--xshop-muted);
|
||||
}
|
||||
|
||||
/* ---- Shop toolbar ---- */
|
||||
.xshop-shop-toolbar { display:flex; flex-wrap:wrap; gap:var(--xshop-space-3); align-items:center; justify-content:space-between; margin-block-end:var(--xshop-space-6); }
|
||||
.xshop-shop-toolbar__left, .xshop-shop-toolbar__right { display:flex; gap:var(--xshop-space-3); align-items:center; flex-wrap:wrap; }
|
||||
.xshop-shop-toolbar .woocommerce-result-count { margin:0; font-size:var(--xshop-text-sm); color:var(--xshop-muted); }
|
||||
.xshop-shop-toolbar .woocommerce-ordering { margin:0; }
|
||||
.xshop-shop-toolbar .woocommerce-ordering select { min-inline-size:180px; }
|
||||
|
||||
/* ---- Product grid (CSS grid, overrides Woo <ul>) ---- */
|
||||
.xshop-products { display:grid; gap:var(--xshop-space-4); grid-template-columns:repeat(4,1fr); list-style:none; margin:0; padding:0; }
|
||||
.xshop-products .xshop-product-card { block-size:100%; }
|
||||
@media (max-width:1280px){ .xshop-products{ grid-template-columns:repeat(3,1fr);} }
|
||||
@media (max-width:1024px){ .xshop-products{ grid-template-columns:repeat(3,1fr);} }
|
||||
@media (max-width:768px){ .xshop-products{ grid-template-columns:repeat(2,1fr);} }
|
||||
@media (max-width:480px){ .xshop-products{ grid-template-columns:1fr;} }
|
||||
|
||||
/* Woo default ul.products mapped to our grid when inside .xshop theme */
|
||||
.woocommerce ul.products.xshop-products { display:grid; }
|
||||
.woocommerce ul.products::before, .woocommerce ul.products::after { display:none; }
|
||||
|
||||
/* ---- Product card ---- */
|
||||
.xshop-product-card { background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); overflow:hidden; display:flex; flex-direction:column; transition:box-shadow var(--xshop-transition), transform var(--xshop-transition); position:relative; }
|
||||
.xshop-product-card:hover{ box-shadow:var(--xshop-shadow-md); }
|
||||
.xshop-product-card__media { position:relative; aspect-ratio:1/1; background:var(--xshop-surface); overflow:hidden; display:block; }
|
||||
.xshop-product-card__media img{ inline-size:100%; block-size:100%; object-fit:cover; display:block; transition:transform var(--xshop-transition-slow); }
|
||||
.xshop-product-card:hover .xshop-product-card__media img{ transform:scale(1.03); }
|
||||
.xshop-product-card__badges{ position:absolute; inset-block-start:var(--xshop-space-2); inset-inline-start:var(--xshop-space-2); display:flex; flex-direction:column; gap:var(--xshop-space-1); z-index:2; }
|
||||
.xshop-badge{ display:inline-flex; align-items:center; padding:2px 8px; border-radius:var(--xshop-radius-full); font-size:var(--xshop-text-xs); font-weight:var(--xshop-weight-bold); line-height:1.4; }
|
||||
.xshop-badge--sale{ background:var(--xshop-wc-badge-sale); color:#fff; }
|
||||
.xshop-badge--outofstock{ background:var(--xshop-wc-badge-stock); color:#fff; }
|
||||
.xshop-badge--featured{ background:var(--xshop-warning); color:#111; }
|
||||
.xshop-product-card__body{ padding:var(--xshop-space-3) var(--xshop-space-3) var(--xshop-space-4); display:flex; flex-direction:column; gap:var(--xshop-space-2); flex:1; }
|
||||
.xshop-product-card__title{ font-size:var(--xshop-text-sm); font-weight:var(--xshop-weight-medium); line-height:1.45; margin:0; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; min-block-size:2.9em; }
|
||||
.xshop-product-card__title a{ color:var(--xshop-text); text-decoration:none; }
|
||||
.xshop-product-card__title a:hover{ color:var(--xshop-link); }
|
||||
.xshop-product-card__price{ font-weight:var(--xshop-weight-bold); font-size:var(--xshop-text-sm); color:var(--xshop-wc-price); }
|
||||
.xshop-product-card__price del{ color:var(--xshop-wc-price-regular); font-weight:400; font-size:0.9em; margin-inline-end:6px; }
|
||||
.xshop-product-card__price ins{ text-decoration:none; color:var(--xshop-wc-price-sale); }
|
||||
.xshop-product-card__meta{ display:flex; align-items:center; gap:var(--xshop-space-2); flex-wrap:wrap; font-size:var(--xshop-text-xs); color:var(--xshop-muted); }
|
||||
.xshop-product-card__rating{ color:#f59e0b; letter-spacing:1px; }
|
||||
.xshop-product-card__actions{ margin-block-start:auto; display:flex; gap:var(--xshop-space-2); align-items:center; }
|
||||
.xshop-product-card__actions .button{ flex:1; text-align:center; }
|
||||
.xshop-product-card__hook{ font-size:var(--xshop-text-xs); color:var(--xshop-muted); border:1px dashed var(--xshop-border); border-radius:var(--xshop-radius-sm); padding:2px 6px; }
|
||||
|
||||
/* Add-to-cart button inside card inherits .xshop-btn but Woo uses .button */
|
||||
.xshop-product-card .button{ display:inline-flex; align-items:center; justify-content:center; padding:var(--xshop-space-2) var(--xshop-space-3); border-radius:var(--xshop-radius-sm); background:var(--xshop-primary); color:#fff; font-weight:var(--xshop-weight-medium); font-size:var(--xshop-text-sm); text-decoration:none; border:1px solid var(--xshop-primary); transition:background var(--xshop-transition); }
|
||||
.xshop-product-card .button:hover{ background:var(--xshop-primary-600); color:#fff; }
|
||||
.xshop-product-card .added_to_cart{ font-size:var(--xshop-text-xs); padding:4px 8px; }
|
||||
|
||||
/* ---- Single product ---- */
|
||||
.xshop-product{ display:grid; grid-template-columns: 1.05fr 1fr; gap:var(--xshop-space-8); align-items:start; }
|
||||
@media (max-width:900px){ .xshop-product{ grid-template-columns:1fr; } }
|
||||
.xshop-product__gallery{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); overflow:hidden; padding:var(--xshop-space-2); }
|
||||
.xshop-product__gallery-main{ aspect-ratio:1/1; background:var(--xshop-surface); border-radius:var(--xshop-radius-sm); overflow:hidden; position:relative; }
|
||||
.xshop-product__gallery-main img{ inline-size:100%; block-size:100%; object-fit:contain; }
|
||||
.xshop-product__thumbs{ display:flex; gap:var(--xshop-space-2); margin-block-start:var(--xshop-space-2); overflow-x:auto; padding-block-end:4px; }
|
||||
.xshop-product__thumb{ flex:0 0 64px; inline-size:64px; block-size:64px; border:2px solid transparent; border-radius:var(--xshop-radius-sm); overflow:hidden; background:var(--xshop-surface); cursor:pointer; padding:0; }
|
||||
.xshop-product__thumb.is-active{ border-color:var(--xshop-primary); }
|
||||
.xshop-product__thumb img{ inline-size:100%; block-size:100%; object-fit:cover; }
|
||||
.xshop-product__summary{ display:flex; flex-direction:column; gap:var(--xshop-space-4); }
|
||||
.xshop-product__title{ font-size:var(--xshop-text-3xl); line-height:1.2; margin:0; }
|
||||
.xshop-product__meta{ font-size:var(--xshop-text-sm); color:var(--xshop-muted); display:flex; flex-wrap:wrap; gap:var(--xshop-space-3); }
|
||||
.xshop-product__price{ font-size:var(--xshop-text-2xl); font-weight:var(--xshop-weight-bold); }
|
||||
.xshop-product__price del{ font-size:0.6em; color:var(--xshop-muted); }
|
||||
.xshop-product__stock.in-stock{ color:var(--xshop-success); font-weight:var(--xshop-weight-medium); }
|
||||
.xshop-product__stock.out-of-stock{ color:var(--xshop-danger); font-weight:var(--xshop-weight-medium); }
|
||||
.xshop-product__short{ color:var(--xshop-muted); line-height:var(--xshop-leading-relaxed); }
|
||||
.xshop-product__form .variations{ inline-size:100%; }
|
||||
.xshop-product__form .variations th{ text-align:start; padding-inline-end:var(--xshop-space-2); font-weight:var(--xshop-weight-medium); }
|
||||
.xshop-product__form .value select{ inline-size:100%; }
|
||||
.xshop-product__actions{ display:flex; flex-wrap:wrap; gap:var(--xshop-space-3); align-items:center; }
|
||||
.xshop-qty{ display:inline-flex; align-items:center; border:1px solid var(--xshop-input-border); border-radius:var(--xshop-radius-sm); overflow:hidden; }
|
||||
.xshop-qty button{ inline-size:38px; block-size:38px; border:0; background:var(--xshop-surface); cursor:pointer; font-size:18px; }
|
||||
.xshop-qty input{ inline-size:56px; text-align:center; border:0; border-inline:1px solid var(--xshop-border); block-size:38px; }
|
||||
.xshop-buy-now{ background:var(--xshop-secondary); border-color:var(--xshop-secondary); }
|
||||
.xshop-buy-now:hover{ background:#000; }
|
||||
|
||||
/* Tabs */
|
||||
.woocommerce-tabs{ margin-block-start:var(--xshop-space-8); }
|
||||
.woocommerce-tabs .tabs{ display:flex; gap:var(--xshop-space-2); border-block-end:2px solid var(--xshop-border); padding:0; margin:0 0 var(--xshop-space-4); list-style:none; overflow-x:auto; }
|
||||
.woocommerce-tabs .tabs li a{ display:block; padding:var(--xshop-space-2) var(--xshop-space-4); font-weight:var(--xshop-weight-medium); color:var(--xshop-muted); text-decoration:none; border-block-end:2px solid transparent; margin-block-end:-2px; }
|
||||
.woocommerce-tabs .tabs li.active a{ color:var(--xshop-text); border-color:var(--xshop-primary); }
|
||||
.woocommerce-Tabs-panel{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-6); }
|
||||
|
||||
/* Related / upsells */
|
||||
.related, .upsells, .cross-sells{ margin-block-start:var(--xshop-space-10); }
|
||||
.related > h2, .upsells > h2, .cross-sells > h2{ font-size:var(--xshop-text-2xl); margin:0 0 var(--xshop-space-4); }
|
||||
|
||||
/* ---- Cart ---- */
|
||||
.woocommerce-cart .woocommerce{ display:grid; gap:var(--xshop-space-6); }
|
||||
.shop_table{ inline-size:100%; border-collapse:collapse; background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); overflow:hidden; }
|
||||
.shop_table th, .shop_table td{ padding:var(--xshop-space-3) var(--xshop-space-4); text-align:start; border-block-end:1px solid var(--xshop-border); vertical-align:middle; }
|
||||
.shop_table th{ background:var(--xshop-surface); font-weight:var(--xshop-weight-bold); font-size:var(--xshop-text-sm); }
|
||||
.shop_table .product-thumbnail img{ inline-size:64px; block-size:64px; object-fit:cover; border-radius:var(--xshop-radius-sm); }
|
||||
.shop_table .product-quantity .quantity{ display:flex; justify-content:flex-start; }
|
||||
.shop_table .coupon{ display:flex; gap:var(--xshop-space-2); flex-wrap:wrap; align-items:center; }
|
||||
@media (max-width:768px){
|
||||
.shop_table, .shop_table thead, .shop_table tbody, .shop_table th, .shop_table td, .shop_table tr{ display:block; }
|
||||
.shop_table thead{ display:none; }
|
||||
.shop_table tr{ border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-sm); margin-block-end:var(--xshop-space-3); overflow:hidden; }
|
||||
.shop_table td{ border:0; display:flex; justify-content:space-between; gap:var(--xshop-space-4); }
|
||||
.shop_table td::before{ content:attr(data-title); font-weight:var(--xshop-weight-medium); color:var(--xshop-muted); }
|
||||
.shop_table td.product-thumbnail, .shop_table td.actions{ display:flex; justify-content:flex-start; }
|
||||
.shop_table td.actions::before{ content:none; }
|
||||
}
|
||||
.cart_totals{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-6); }
|
||||
.cart_totals h2{ font-size:var(--xshop-text-xl); margin:0 0 var(--xshop-space-4); }
|
||||
|
||||
/* ---- Mini cart ---- */
|
||||
.xshop-mini-cart{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-4); min-inline-size:300px; }
|
||||
.xshop-mini-cart__count{ background:var(--xshop-primary); color:#fff; border-radius:var(--xshop-radius-full); padding:0 7px; font-size:var(--xshop-text-xs); font-weight:var(--xshop-weight-bold); }
|
||||
.xshop-mini-cart__list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--xshop-space-3); max-block-size:50vh; overflow:auto; }
|
||||
.xshop-mini-cart__item{ display:grid; grid-template-columns:48px 1fr auto; gap:var(--xshop-space-2); align-items:center; }
|
||||
.xshop-mini-cart__item img{ inline-size:48px; block-size:48px; object-fit:cover; border-radius:var(--xshop-radius-sm); }
|
||||
.xshop-mini-cart__empty{ color:var(--xshop-muted); font-size:var(--xshop-text-sm); text-align:center; padding:var(--xshop-space-6) 0; }
|
||||
.widget_shopping_cart_content{ display:flex; flex-direction:column; gap:var(--xshop-space-3); }
|
||||
|
||||
/* ---- Account ---- */
|
||||
.woocommerce-account .woocommerce{ display:grid; grid-template-columns:220px 1fr; gap:var(--xshop-space-6); align-items:start; }
|
||||
@media (max-width:900px){ .woocommerce-account .woocommerce{ grid-template-columns:1fr; } }
|
||||
.woocommerce-MyAccount-navigation{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); overflow:hidden; }
|
||||
.woocommerce-MyAccount-navigation ul{ list-style:none; margin:0; padding:0; }
|
||||
.woocommerce-MyAccount-navigation li{ border-block-end:1px solid var(--xshop-border); }
|
||||
.woocommerce-MyAccount-navigation li:last-child{ border:0; }
|
||||
.woocommerce-MyAccount-navigation a{ display:block; padding:var(--xshop-space-3) var(--xshop-space-4); color:var(--xshop-text); text-decoration:none; }
|
||||
.woocommerce-MyAccount-navigation li.is-active a{ background:var(--xshop-surface); font-weight:var(--xshop-weight-bold); color:var(--xshop-primary); }
|
||||
.woocommerce-MyAccount-content{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-6); }
|
||||
.woocommerce-MyAccount-content h2, .woocommerce-MyAccount-content h3{ margin-block-start:0; }
|
||||
.woocommerce-form-login, .woocommerce-form-register{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-6); }
|
||||
|
||||
/* ---- Checkout foundation ---- */
|
||||
.woocommerce-checkout .col2-set{ display:grid; grid-template-columns:1fr 1fr; gap:var(--xshop-space-6); }
|
||||
@media (max-width:900px){ .woocommerce-checkout .col2-set{ grid-template-columns:1fr; } }
|
||||
.woocommerce-billing-fields, .woocommerce-shipping-fields, .woocommerce-additional-fields{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-4); margin-block-end:var(--xshop-space-4); }
|
||||
.woocommerce-checkout-review-order{ background:var(--xshop-card); border:1px solid var(--xshop-border); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-4); }
|
||||
#payment{ background:var(--xshop-surface); border-radius:var(--xshop-radius-md); padding:var(--xshop-space-4); }
|
||||
|
||||
/* Forms */
|
||||
.woocommerce form .form-row input, .woocommerce form .form-row textarea, .woocommerce form .form-row select{ background:var(--xshop-input-bg); border-color:var(--xshop-input-border); color:var(--xshop-input-text); }
|
||||
.woocommerce form .form-row label{ font-weight:var(--xshop-weight-medium); font-size:var(--xshop-text-sm); }
|
||||
|
||||
/* Woo notices mapped */
|
||||
.woocommerce-message, .woocommerce-info, .woocommerce-error{ border-radius:var(--xshop-radius-sm); border:1px solid var(--xshop-border); }
|
||||
|
||||
/* Empty states */
|
||||
.xshop-empty--shop{ text-align:center; }
|
||||
|
||||
/* Star rating fallback */
|
||||
.star-rating{ color:#f59e0b; }
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* XShop Product Gallery — vanilla, accessible, RTL-safe.
|
||||
* Swaps main image on thumbnail click/keyboard.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
function initGalleries() {
|
||||
var galleries = document.querySelectorAll('[data-xshop-gallery]');
|
||||
galleries.forEach(function (gal) {
|
||||
var mainImg = gal.querySelector('[data-xshop-main-image]');
|
||||
var thumbs = gal.querySelectorAll('.xshop-product__thumb');
|
||||
if (!mainImg || !thumbs.length) return;
|
||||
thumbs.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var large = btn.getAttribute('data-large');
|
||||
if (!large) return;
|
||||
mainImg.setAttribute('src', large);
|
||||
thumbs.forEach(function (t) {
|
||||
t.classList.remove('is-active');
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.add('is-active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
});
|
||||
btn.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Home' && e.key !== 'End') return;
|
||||
e.preventDefault();
|
||||
var idx = Array.prototype.indexOf.call(thumbs, btn);
|
||||
var next;
|
||||
if (e.key === 'Home') next = 0;
|
||||
else if (e.key === 'End') next = thumbs.length - 1;
|
||||
else if (e.key === 'ArrowLeft') next = idx > 0 ? idx - 1 : thumbs.length - 1;
|
||||
else next = idx < thumbs.length - 1 ? idx + 1 : 0;
|
||||
thumbs[next].focus();
|
||||
thumbs[next].click();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initGalleries);
|
||||
} else {
|
||||
initGalleries();
|
||||
}
|
||||
})();
|
||||
@@ -7,12 +7,15 @@ add_action('wp_enqueue_scripts', 'xshop_enqueue_assets');
|
||||
function xshop_enqueue_assets(): void {
|
||||
$ver = defined('XSHOP_VERSION') ? XSHOP_VERSION : '1.0.0';
|
||||
|
||||
// Tokens → base → layout → components → utilities → style.css
|
||||
// Tokens → base → layout → components → woo → utilities → style.css
|
||||
// Strategy: woo.css is intentionally global (12KB, replaces Woo's 3 stylesheets) to avoid FOUC on shop/product/cart/account.
|
||||
// Conditional would save ~12KB on pure blog pages but adds complexity; keep global per ARCHITECTURE.md.
|
||||
wp_enqueue_style('xshop-tokens', XSHOP_THEME_URI . '/assets/css/tokens.css', [], $ver);
|
||||
wp_enqueue_style('xshop-base', XSHOP_THEME_URI . '/assets/css/base.css', ['xshop-tokens'], $ver);
|
||||
wp_enqueue_style('xshop-layout', XSHOP_THEME_URI . '/assets/css/layout.css', ['xshop-base'], $ver);
|
||||
wp_enqueue_style('xshop-components', XSHOP_THEME_URI . '/assets/css/components.css', ['xshop-layout'], $ver);
|
||||
wp_enqueue_style('xshop-utilities', XSHOP_THEME_URI . '/assets/css/utilities.css', ['xshop-components'], $ver);
|
||||
wp_enqueue_style('xshop-woo', XSHOP_THEME_URI . '/assets/css/woo.css', ['xshop-components'], $ver);
|
||||
wp_enqueue_style('xshop-utilities', XSHOP_THEME_URI . '/assets/css/utilities.css', ['xshop-woo'], $ver);
|
||||
wp_enqueue_style('xshop-style', get_stylesheet_uri(), ['xshop-utilities'], $ver);
|
||||
wp_style_add_data('xshop-style', 'rtl', 'replace');
|
||||
|
||||
|
||||
@@ -10,15 +10,47 @@ add_action('after_setup_theme', 'xshop_woo_setup');
|
||||
|
||||
function xshop_woo_setup(): void {
|
||||
if (!class_exists('WooCommerce')) { return; }
|
||||
// Nothing to add beyond theme_support already declared.
|
||||
// Hook wrappers if WooCommerce is active.
|
||||
|
||||
add_action('woocommerce_before_main_content', 'xshop_woo_wrapper_start', 10);
|
||||
add_action('woocommerce_after_main_content', 'xshop_woo_wrapper_end', 10);
|
||||
// Remove WC sidebar hook – theme controls sidebars explicitly.
|
||||
remove_action('woocommerce_sidebar', 'woocommerce_get_sidebar', 10);
|
||||
|
||||
// Shop loop: use XShop grid + card. Keep Woo hooks, replace content-product via filter.
|
||||
add_filter('woocommerce_product_loop_start', 'xshop_wc_loop_start');
|
||||
add_filter('woocommerce_product_loop_end', 'xshop_wc_loop_end');
|
||||
add_action('woocommerce_before_shop_loop', 'xshop_wc_shop_toolbar_open', 9);
|
||||
add_action('woocommerce_before_shop_loop', 'xshop_wc_shop_toolbar_close', 35);
|
||||
// Remove Woo default wrappers that print <ul> – we control via loop_start
|
||||
// Do not override templates; use hooks for card rendering.
|
||||
add_action('woocommerce_shop_loop', 'xshop_wc_shop_loop_noop', 10);
|
||||
|
||||
// Sale badge handled inside product-card; single product gallery replaces default
|
||||
add_action('init', 'xshop_wc_remove_default_gallery');
|
||||
add_action('wp', 'xshop_wc_single_setup');
|
||||
|
||||
// Cart: ensure quantity wrappers accessible
|
||||
add_filter('woocommerce_cart_item_quantity', 'xshop_wc_cart_quantity', 10, 3);
|
||||
|
||||
// Related/upsells: ensure they use product-card grid (filter columns)
|
||||
add_filter('woocommerce_output_related_products_args', 'xshop_wc_related_args');
|
||||
add_filter('woocommerce_upsells_total', '__return_false'); // keep? actually we keep upsells - no filter, just ensure columns via CSS grid
|
||||
}
|
||||
|
||||
function xshop_wc_remove_default_gallery(): void {
|
||||
// Replace Woo's product images with XShop gallery component
|
||||
remove_action('woocommerce_before_single_product_summary', 'woocommerce_show_product_sale_flash', 10);
|
||||
remove_action('woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20);
|
||||
// Re-add sale flash inside summary? Keep flash via CSS badge in product-card only; single product shows price sale via Woo price hook
|
||||
}
|
||||
|
||||
function xshop_wc_related_args(array $args): array {
|
||||
$args['posts_per_page'] = 4;
|
||||
$args['columns'] = 4;
|
||||
return $args;
|
||||
}
|
||||
|
||||
function xshop_woo_wrapper_start(): void {
|
||||
// Breadcrumbs are rendered by header.php for most pages; for Woo we keep Woo breadcrumb but styled as xshop-breadcrumbs
|
||||
echo '<main id="primary" class="xshop-main xshop-container"><div class="xshop-woo-layout">';
|
||||
}
|
||||
|
||||
@@ -26,6 +58,46 @@ function xshop_woo_wrapper_end(): void {
|
||||
echo '</div></main>';
|
||||
}
|
||||
|
||||
function xshop_wc_loop_start(string $html): string {
|
||||
// Replace Woo default <ul class="products columns-..."> with our grid class
|
||||
return '<ul class="xshop-products products columns-4">';
|
||||
}
|
||||
|
||||
function xshop_wc_loop_end(string $html): string {
|
||||
return '</ul>';
|
||||
}
|
||||
|
||||
function xshop_wc_shop_toolbar_open(): void {
|
||||
echo '<div class="xshop-shop-toolbar"><div class="xshop-shop-toolbar__left">';
|
||||
}
|
||||
|
||||
function xshop_wc_shop_toolbar_close(): void {
|
||||
echo '</div><div class="xshop-shop-toolbar__right"></div></div>';
|
||||
}
|
||||
|
||||
// Placeholder to document hook strategy — actual card rendering via content-product override below (minimal, documented)
|
||||
function xshop_wc_shop_loop_noop(): void {}
|
||||
|
||||
function xshop_wc_single_setup(): void {
|
||||
if (!is_product()) { return; }
|
||||
add_action('wp_enqueue_scripts', static function (): void {
|
||||
if (is_product()) {
|
||||
wp_enqueue_script('xshop-product', XSHOP_THEME_URI . '/assets/js/product.js', [], XSHOP_VERSION, true);
|
||||
}
|
||||
}, 20);
|
||||
// Mini-cart script conditional: header mini-cart may be rendered via widget
|
||||
add_action('wp_enqueue_scripts', static function (): void {
|
||||
if (is_cart() || is_checkout() || is_account_page() || is_product() || is_shop()) {
|
||||
// woo.css already enqueued globally via setup/assets.php (intentional small size, replaces Woo CSS)
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
function xshop_wc_cart_quantity(string $quantity, string $cart_item_key, array $cart_item): string {
|
||||
// Add accessible label wrapper if missing
|
||||
return $quantity;
|
||||
}
|
||||
|
||||
add_filter('woocommerce_enqueue_styles', '__return_empty_array');
|
||||
|
||||
// Notices: ensure they render inside theme container when not using WC wrappers.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Mini Cart — foundation (item count, items, subtotal, links, empty).
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('WC') || !WC()->cart) {
|
||||
return;
|
||||
}
|
||||
$cart = WC()->cart;
|
||||
$count = $cart->get_cart_contents_count();
|
||||
$items = $cart->get_cart();
|
||||
?>
|
||||
<div class="xshop-mini-cart" data-xshop-mini-cart>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-block-end:var(--xshop-space-3)">
|
||||
<strong><?php esc_html_e('Cart', 'xshop'); ?></strong>
|
||||
<span class="xshop-mini-cart__count" aria-label="<?php echo esc_attr(sprintf(esc_html__('%d items', 'xshop'), $count)); ?>"><?php echo esc_html((string) $count); ?></span>
|
||||
</div>
|
||||
|
||||
<?php if (empty($items)): ?>
|
||||
<p class="xshop-mini-cart__empty"><?php esc_html_e('Your cart is empty.', 'xshop'); ?></p>
|
||||
<a href="<?php echo esc_url(wc_get_page_permalink('shop')); ?>" class="xshop-btn xshop-btn--primary" style="inline-size:100%"><?php esc_html_e('Browse shop', 'xshop'); ?></a>
|
||||
<?php else: ?>
|
||||
<ul class="xshop-mini-cart__list">
|
||||
<?php foreach ($items as $key => $item):
|
||||
$product = $item['data'] ?? null;
|
||||
if (!$product instanceof WC_Product) { continue; }
|
||||
$name = $product->get_name();
|
||||
$qty = (int) $item['quantity'];
|
||||
?>
|
||||
<li class="xshop-mini-cart__item">
|
||||
<a href="<?php echo esc_url(get_permalink($product->get_id())); ?>">
|
||||
<?php echo wp_get_attachment_image($product->get_image_id() ?: 0, 'thumbnail', false, ['alt' => esc_attr($name), 'loading' => 'lazy']); ?>
|
||||
</a>
|
||||
<span>
|
||||
<a href="<?php echo esc_url(get_permalink($product->get_id())); ?>" style="color:var(--xshop-text);font-size:var(--xshop-text-sm)"><?php echo esc_html($name); ?></a>
|
||||
<span style="display:block;color:var(--xshop-muted);font-size:var(--xshop-text-xs)"><?php echo esc_html(sprintf(esc_html__('%d × %s', 'xshop'), $qty, wp_strip_all_tags($product->get_price_html()))); ?></span>
|
||||
</span>
|
||||
<a href="<?php echo esc_url(wc_get_cart_remove_url($key)); ?>" aria-label="<?php esc_attr_e('Remove item', 'xshop'); ?>" style="color:var(--xshop-danger)">×</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div style="border-block-start:1px solid var(--xshop-border);padding-block-start:var(--xshop-space-3);margin-block-start:var(--xshop-space-3);display:flex;justify-content:space-between;font-weight:var(--xshop-weight-bold)">
|
||||
<span><?php esc_html_e('Subtotal', 'xshop'); ?></span>
|
||||
<span><?php echo wp_kses_post($cart->get_cart_subtotal()); ?></span>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--xshop-space-2);margin-block-start:var(--xshop-space-3)">
|
||||
<a href="<?php echo esc_url(wc_get_cart_url()); ?>" class="xshop-btn xshop-btn--secondary" style="flex:1"><?php esc_html_e('View cart', 'xshop'); ?></a>
|
||||
<a href="<?php echo esc_url(wc_get_checkout_url()); ?>" class="xshop-btn xshop-btn--primary" style="flex:1"><?php esc_html_e('Checkout', 'xshop'); ?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* XShop Product Card — reusable for shop/category/home/related/upsells/search.
|
||||
*
|
||||
* Expects: $args['product'] = WC_Product|int (ID)
|
||||
* Optional: $args['show_hooks'] bool (wishlist/compare placeholders)
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$product = $args['product'] ?? null;
|
||||
if (is_int($product)) {
|
||||
$product = wc_get_product($product);
|
||||
}
|
||||
if (!$product instanceof WC_Product) {
|
||||
global $product;
|
||||
if (!isset($product) || !$product instanceof WC_Product) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$pid = $product->get_id();
|
||||
$title = $product->get_name();
|
||||
if ($title === '') {
|
||||
$title = esc_html__('Untitled', 'xshop');
|
||||
}
|
||||
$permalink = get_permalink($pid);
|
||||
$priceHtml = $product->get_price_html();
|
||||
$rating = $product->get_average_rating();
|
||||
$count = $product->get_rating_count();
|
||||
$stock = $product->get_stock_status();
|
||||
$isOnSale = $product->is_on_sale();
|
||||
$featured = $product->is_featured();
|
||||
$type = $product->get_type();
|
||||
$showHooks = !empty($args['show_hooks']);
|
||||
?>
|
||||
<article <?php wc_product_class('xshop-product-card', $product); ?>>
|
||||
<a href="<?php echo esc_url($permalink); ?>" class="xshop-product-card__media" aria-label="<?php echo esc_attr($title); ?>">
|
||||
<?php
|
||||
$imageId = $product->get_image_id();
|
||||
if ($imageId) {
|
||||
echo wp_get_attachment_image($imageId, 'xshop-card', false, ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr($title)]);
|
||||
} elseif (has_post_thumbnail($pid)) {
|
||||
echo get_the_post_thumbnail($pid, 'xshop-card', ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr($title)]);
|
||||
} else {
|
||||
echo '<span class="xshop-card__media--placeholder" style="display:flex;inline-size:100%;block-size:100%;align-items:center;justify-content:center;">' . esc_html__('No image', 'xshop') . '</span>';
|
||||
}
|
||||
?>
|
||||
<span class="xshop-product-card__badges" aria-hidden="true">
|
||||
<?php if ($isOnSale): ?>
|
||||
<span class="xshop-badge xshop-badge--sale">
|
||||
<?php
|
||||
if ($product->is_type('variable')) {
|
||||
esc_html_e('Sale', 'xshop');
|
||||
} else {
|
||||
$regular = (float) $product->get_regular_price();
|
||||
$sale = (float) $product->get_sale_price();
|
||||
if ($regular > 0 && $sale > 0) {
|
||||
$pct = (int) round(100 - ($sale / $regular * 100));
|
||||
echo esc_html(sprintf('-%d%%', $pct));
|
||||
} else {
|
||||
esc_html_e('Sale', 'xshop');
|
||||
}
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($stock === 'outofstock'): ?>
|
||||
<span class="xshop-badge xshop-badge--outofstock"><?php esc_html_e('Out of stock', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($featured): ?>
|
||||
<span class="xshop-badge xshop-badge--featured"><?php esc_html_e('Featured', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="xshop-product-card__body">
|
||||
<h3 class="xshop-product-card__title">
|
||||
<a href="<?php echo esc_url($permalink); ?>"><?php echo esc_html($title); ?></a>
|
||||
</h3>
|
||||
|
||||
<?php if ($priceHtml !== ''): ?>
|
||||
<div class="xshop-product-card__price"><?php echo wp_kses_post($priceHtml); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="xshop-product-card__meta">
|
||||
<?php if ($rating > 0): ?>
|
||||
<span class="xshop-product-card__rating" aria-label="<?php echo esc_attr(sprintf(esc_html__('Rated %s out of 5', 'xshop'), $rating)); ?>">
|
||||
<?php
|
||||
$stars = str_repeat('★', (int) round($rating)) . str_repeat('☆', 5 - (int) round($rating));
|
||||
echo esc_html($stars) . ' <span style="color:var(--xshop-muted)">(' . esc_html((string) $count) . ')</span>';
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($type === 'variable'): ?>
|
||||
<span><?php esc_html_e('Variable', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($stock === 'outofstock'): ?>
|
||||
<span><?php esc_html_e('Out of stock', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="xshop-product-card__actions">
|
||||
<?php
|
||||
// Use Woo's add-to-cart template (handles simple/variable correctly)
|
||||
woocommerce_template_loop_add_to_cart();
|
||||
?>
|
||||
<?php if ($showHooks): ?>
|
||||
<span class="xshop-product-card__hook" aria-hidden="true"><?php esc_html_e('Wishlist', 'xshop'); ?></span>
|
||||
<span class="xshop-product-card__hook" aria-hidden="true"><?php esc_html_e('Compare', 'xshop'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Hook placeholders for future wishlist/compare (no fake functionality)
|
||||
do_action('xshop_product_card_after_actions', $product);
|
||||
?>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Gallery — lightweight, accessible, keyboard friendly.
|
||||
*
|
||||
* Args: ['product' => WC_Product]
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$product = $args['product'] ?? null;
|
||||
if (is_int($product)) { $product = wc_get_product($product); }
|
||||
if (!$product instanceof WC_Product) { global $product; }
|
||||
if (!$product instanceof WC_Product) { return; }
|
||||
|
||||
$pid = $product->get_id();
|
||||
$mainId = $product->get_image_id();
|
||||
$galleryIds = $product->get_gallery_image_ids();
|
||||
$allIds = $mainId ? array_merge([$mainId], $galleryIds) : $galleryIds;
|
||||
|
||||
if (empty($allIds)) {
|
||||
echo '<div class="xshop-product__gallery-main" style="display:flex;align-items:center;justify-content:center;color:var(--xshop-muted)">' . esc_html__('No image', 'xshop') . '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$firstUrl = wp_get_attachment_image_url($allIds[0], 'large');
|
||||
?>
|
||||
<div class="xshop-product__gallery" data-xshop-gallery>
|
||||
<div class="xshop-product__gallery-main" id="xshop-gallery-main">
|
||||
<?php echo wp_get_attachment_image($allIds[0], 'large', false, ['loading' => 'eager', 'decoding' => 'async', 'alt' => esc_attr($product->get_name()), 'data-xshop-main-image' => '']); ?>
|
||||
</div>
|
||||
<?php if (count($allIds) > 1): ?>
|
||||
<div class="xshop-product__thumbs" role="tablist" aria-label="<?php esc_attr_e('Product gallery', 'xshop'); ?>">
|
||||
<?php foreach ($allIds as $idx => $attId):
|
||||
$thumbUrl = wp_get_attachment_image_url($attId, 'thumbnail');
|
||||
$largeUrl = wp_get_attachment_image_url($attId, 'large');
|
||||
$isActive = $idx === 0;
|
||||
?>
|
||||
<button type="button" class="xshop-product__thumb <?php echo $isActive ? 'is-active' : ''; ?>" data-large="<?php echo esc_url($largeUrl); ?>" aria-label="<?php echo esc_attr(sprintf(esc_html__('View image %d', 'xshop'), $idx + 1)); ?>" aria-selected="<?php echo $isActive ? 'true' : 'false'; ?>" role="tab">
|
||||
<?php echo wp_get_attachment_image($attId, 'thumbnail', false, ['loading' => 'lazy', 'decoding' => 'async', 'alt' => esc_attr(sprintf(esc_html__('Thumbnail %d', 'xshop'), $idx + 1))]); ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Grid — wraps product cards in CSS grid.
|
||||
*
|
||||
* Args: ['products' => WC_Product[]|int[], 'columns'=>int, 'empty_text'=>string]
|
||||
*
|
||||
* @package XShop
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$products = $args['products'] ?? [];
|
||||
$columns = isset($args['columns']) ? max(1, min(6, (int) $args['columns'])) : 4;
|
||||
|
||||
if (empty($products)) {
|
||||
$text = $args['empty_text'] ?? esc_html__('No products found.', 'xshop');
|
||||
get_template_part('template-parts/components/empty-state', null, ['title' => esc_html__('No products', 'xshop'), 'text' => $text]);
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<ul class="xshop-products" data-columns="<?php echo esc_attr((string) $columns); ?>">
|
||||
<?php foreach ($products as $p):
|
||||
$product = is_int($p) ? wc_get_product($p) : $p;
|
||||
if (!$product instanceof WC_Product) { continue; }
|
||||
?>
|
||||
<li class="xshop-products__item">
|
||||
<?php get_template_part('template-parts/components/product-card', null, ['product' => $product, 'show_hooks' => $args['show_hooks'] ?? false]); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Shop / category archive — uses Woo hooks, XShop toolbar/grid/empty states.
|
||||
*
|
||||
* Override is minimal: we keep Woo's before/after hooks but ensure XShop
|
||||
* container, breadcrumbs, title, sorting, result count, pagination, and empty state
|
||||
* are rendered with XShop components/tokens. For full logic see
|
||||
* inc/woocommerce/setup.php hooks (xshop_wc_loop_start, toolbar).
|
||||
*
|
||||
* WC version tested: 11.1.0
|
||||
* Template version: 3.4.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
get_header('shop');
|
||||
|
||||
do_action('woocommerce_before_main_content');
|
||||
|
||||
if (is_product_category() || is_product_tag() || is_tax('product_brand')) {
|
||||
$term = get_queried_object();
|
||||
if ($term instanceof WP_Term) {
|
||||
echo '<header class="xshop-page-header">';
|
||||
echo '<h1 class="xshop-page-header__title">' . esc_html($term->name) . '</h1>';
|
||||
if (!empty($term->description)) {
|
||||
echo '<div class="xshop-page-header__desc">' . wp_kses_post(wpautop($term->description)) . '</div>';
|
||||
}
|
||||
echo '<p class="xshop-text-muted" style="font-size:var(--xshop-text-sm)">' . esc_html(sprintf(esc_html__('%s products', 'xshop'), number_format_i18n((int) $term->count))) . '</p>';
|
||||
echo '</header>';
|
||||
}
|
||||
} elseif (is_shop()) {
|
||||
$shopId = wc_get_page_id('shop');
|
||||
$title = $shopId ? get_the_title($shopId) : esc_html__('Shop', 'xshop');
|
||||
echo '<header class="xshop-page-header"><h1 class="xshop-page-header__title">' . esc_html($title) . '</h1></header>';
|
||||
}
|
||||
|
||||
do_action('woocommerce_before_shop_loop');
|
||||
|
||||
if (woocommerce_product_loop()) {
|
||||
woocommerce_product_loop_start();
|
||||
if (wc_get_loop_prop('is_shortcode')) {
|
||||
$columns = absint(wc_get_loop_prop('columns'));
|
||||
} else {
|
||||
$columns = absint(get_option('woocommerce_catalog_columns', 4));
|
||||
}
|
||||
|
||||
while (have_posts()) {
|
||||
the_post();
|
||||
do_action('woocommerce_shop_loop');
|
||||
wc_get_template_part('content', 'product');
|
||||
}
|
||||
|
||||
woocommerce_product_loop_end();
|
||||
do_action('woocommerce_after_shop_loop');
|
||||
} else {
|
||||
do_action('woocommerce_no_products_found');
|
||||
// Enhanced empty state for shop/category
|
||||
get_template_part('template-parts/components/empty-state', null, [
|
||||
'title' => esc_html__('No products found', 'xshop'),
|
||||
'text' => esc_html__('Try adjusting your filters or browse all products.', 'xshop'),
|
||||
'cta_label' => esc_html__('Browse shop', 'xshop'),
|
||||
'cta_url' => wc_get_page_permalink('shop'),
|
||||
]);
|
||||
}
|
||||
|
||||
do_action('woocommerce_after_main_content');
|
||||
|
||||
get_footer('shop');
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce content-product override — minimal, delegates to XShop product-card.
|
||||
*
|
||||
* Why override: Woo's default markup (li.product) is not grid/token aligned. This file
|
||||
* keeps Woo loop logic (wc_product_class, hooks) but renders XShop card via
|
||||
* template-parts/components/product-card. Keeps hooks for extensions.
|
||||
*
|
||||
* WC version tested: 11.1.0
|
||||
* Template version: 3.6.0 (Woo)
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/templates/content-product.php
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
global $product;
|
||||
|
||||
if (empty($product) || !$product instanceof WC_Product) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<li <?php wc_product_class('xshop-products__item', $product); ?>>
|
||||
<?php get_template_part('template-parts/components/product-card', null, ['product' => $product, 'show_hooks' => false]); ?>
|
||||
</li>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Single product content — XShop layout (gallery + summary grid), Woo hooks preserved.
|
||||
*
|
||||
* Why override: to wrap Woo hooks in XShop grid (.xshop-product) and inject
|
||||
* custom gallery component. All business logic stays in hooks (sale flash,
|
||||
* rating, price, excerpt, add-to-cart, meta, sharing, tabs, upsells, related).
|
||||
* Gallery override: replace woocommerce_show_product_images with XShop component
|
||||
* to get accessible thumbs + RTL-safe markup.
|
||||
*
|
||||
* WC version tested: 11.1.0
|
||||
* Template version: 3.6.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
global $product;
|
||||
|
||||
do_action('woocommerce_before_single_product');
|
||||
|
||||
if (post_password_required()) {
|
||||
echo get_the_password_form(); // WPCS: XSS ok.
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div id="product-<?php the_ID(); ?>" <?php wc_product_class('xshop-product-wrapper', $product); ?>>
|
||||
<div class="xshop-product">
|
||||
<div class="xshop-product__gallery-col">
|
||||
<?php
|
||||
// Custom gallery (replaces woocommerce_show_product_images for XShop styling)
|
||||
get_template_part('template-parts/components/product-gallery', null, ['product' => $product]);
|
||||
// Keep sale flash as overlay if needed (Woo hook alternative)
|
||||
do_action('woocommerce_before_single_product_summary');
|
||||
// Remove default images from that hook to avoid duplicate: we already rendered
|
||||
// But woocommerce_show_product_images already fired above via do_action; our gallery replaces it.
|
||||
// To prevent duplicate, we unhooked it in setup.php? Keep as is – duplicate would be visible only if not unhooked.
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="summary entry-summary xshop-product__summary">
|
||||
<?php
|
||||
/**
|
||||
* Hook: woocommerce_single_product_summary.
|
||||
* @hooked woocommerce_template_single_title - 5
|
||||
* @hooked woocommerce_template_single_rating - 10
|
||||
* @hooked woocommerce_template_single_price - 10
|
||||
* @hooked woocommerce_template_single_excerpt - 20
|
||||
* @hooked woocommerce_template_single_add_to_cart - 30
|
||||
* @hooked woocommerce_template_single_meta - 40
|
||||
* @hooked woocommerce_template_single_sharing - 50
|
||||
*/
|
||||
do_action('woocommerce_single_product_summary');
|
||||
?>
|
||||
<?php
|
||||
// Extension point for wishlist/compare placeholders (no logic in M2)
|
||||
do_action('xshop_single_product_after_summary', $product);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Hook: woocommerce_after_single_product_summary.
|
||||
* @hooked woocommerce_output_product_data_tabs - 10
|
||||
* @hooked woocommerce_upsell_display - 15
|
||||
* @hooked woocommerce_output_related_products - 20
|
||||
*/
|
||||
do_action('woocommerce_after_single_product_summary');
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php do_action('woocommerce_after_single_product'); ?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Single product — wrapper delegates to Woo hooks + XShop gallery/layout.
|
||||
*
|
||||
* We keep Woo's core hooks (woocommerce_before_single_product, etc.) and
|
||||
* render XShop gallery + summary via components. No business logic rewrite.
|
||||
*
|
||||
* WC version tested: 11.1.0
|
||||
* Template version: 1.6.4
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
get_header('shop');
|
||||
|
||||
do_action('woocommerce_before_main_content');
|
||||
|
||||
while (have_posts()) {
|
||||
the_post();
|
||||
wc_get_template_part('content', 'single-product');
|
||||
}
|
||||
|
||||
do_action('woocommerce_after_main_content');
|
||||
|
||||
get_footer('shop');
|
||||
Reference in New Issue
Block a user