feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -19,6 +19,12 @@ class Assets
|
||||
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
|
||||
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
|
||||
add_action('woocommerce_before_main_content', [self::class, 'inject_spa_mount_point'], 5);
|
||||
|
||||
// Hide admin bar if configured
|
||||
$settings = get_option('woonoow_appearance_settings', []);
|
||||
if (!empty($settings['general']['hide_admin_bar'])) {
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,13 +121,15 @@ class Assets
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in full mode and not on a page with shortcode
|
||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
|
||||
// Get appearance settings for unified spa_mode check
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||
|
||||
if ($mode === 'full') {
|
||||
if ($spa_mode === 'full') {
|
||||
// Only inject if the mount point doesn't already exist (from shortcode)
|
||||
echo '<div id="woonoow-customer-app" data-page="shop"><div class="woonoow-loading"><p>Loading...</p></div></div>';
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||
$current_path = parse_url($request_uri, PHP_URL_PATH);
|
||||
echo '<div id="woonoow-customer-app" data-page="shop" data-initial-route="' . esc_attr($current_path) . '"><div class="woonoow-loading"><p>Loading...</p></div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +235,14 @@ class Assets
|
||||
// If SPA Entry Page is WP frontpage, base path is /, otherwise use Entry Page slug
|
||||
$base_path = $is_spa_wp_frontpage ? '' : ($spa_page ? '/' . $spa_page->post_name : '/store');
|
||||
|
||||
// When injecting into a CPT or structural page, the URL does not start with the SPA base path.
|
||||
// E.g., /desain-mockup... instead of /store/desain-mockup...
|
||||
// For these pages, we must force the base path to empty so BrowserRouter starts from the root.
|
||||
if (is_singular() && (!isset($spa_page) || get_queried_object_id() !== $spa_page->ID)) {
|
||||
// If we're on a singular page that isn't the SPA Entry Page, it's a structural page or CPT
|
||||
$base_path = '';
|
||||
}
|
||||
|
||||
// Check if BrowserRouter is enabled (default: true for SEO)
|
||||
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
||||
|
||||
@@ -275,20 +291,27 @@ class Assets
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should load customer-spa assets
|
||||
* Check if we should load assets on this page
|
||||
*/
|
||||
private static function should_load_assets()
|
||||
public static function should_load_assets()
|
||||
{
|
||||
global $post;
|
||||
// Don't load on admin pages
|
||||
if (is_admin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're serving SPA directly (set by serve_spa_for_frontpage_routes)
|
||||
// Force load if constant is defined (e.g. for preview)
|
||||
if (defined('WOONOOW_SERVE_SPA') && WOONOOW_SERVE_SPA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we're on a frontpage SPA route (by URL detection)
|
||||
if (self::is_frontpage_spa_route()) {
|
||||
return true;
|
||||
// Get SPA mode from appearance settings
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||
|
||||
// Check if SPA is completely disabled
|
||||
if ($mode === 'disabled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First check: Is this a designated SPA page?
|
||||
@@ -296,39 +319,29 @@ class Assets
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get SPA mode from appearance settings (the correct source)
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||
// Check if we're on a frontpage SPA route
|
||||
if (self::is_frontpage_spa_route()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If disabled, only load for pages with shortcodes
|
||||
if ($mode === 'disabled') {
|
||||
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
|
||||
if (function_exists('is_shop') && is_shop()) {
|
||||
$shop_page_id = get_option('woocommerce_shop_page_id');
|
||||
if ($shop_page_id) {
|
||||
$shop_page = get_post($shop_page_id);
|
||||
if ($shop_page && has_shortcode($shop_page->post_content, 'woonoow_shop')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// For structural pages (is_singular('page'))
|
||||
if (is_singular('page')) {
|
||||
$page_id = get_queried_object_id();
|
||||
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||
if (!empty($structure) && !empty($structure['sections'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for shortcodes on regular pages
|
||||
if ($post) {
|
||||
if (has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||
return true;
|
||||
}
|
||||
if (has_shortcode($post->post_content, 'woonoow_cart')) {
|
||||
return true;
|
||||
}
|
||||
if (has_shortcode($post->post_content, 'woonoow_checkout')) {
|
||||
return true;
|
||||
}
|
||||
if (has_shortcode($post->post_content, 'woonoow_account')) {
|
||||
// For CPTs with WooNooW templates
|
||||
if (is_singular() && !is_singular('page')) {
|
||||
$post_type = get_post_type();
|
||||
if (!in_array($post_type, ['product', 'shop_order', 'shop_coupon'])) {
|
||||
$wn_template = get_option("wn_template_{$post_type}", null);
|
||||
if (!empty($wn_template) && !empty($wn_template['sections'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Full SPA mode - load on all WooCommerce pages
|
||||
@@ -353,6 +366,7 @@ class Assets
|
||||
|
||||
// Checkout-Only mode - load only on specific pages
|
||||
if ($mode === 'checkout_only') {
|
||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
|
||||
|
||||
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
|
||||
@@ -370,6 +384,7 @@ class Assets
|
||||
return false;
|
||||
}
|
||||
|
||||
global $post;
|
||||
// Check if current page has WooNooW shortcodes
|
||||
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||
return true;
|
||||
|
||||
@@ -283,8 +283,8 @@ class ShopController
|
||||
|
||||
// Add detailed info if requested
|
||||
if ($detailed) {
|
||||
$data['description'] = $product->get_description();
|
||||
$data['short_description'] = $product->get_short_description();
|
||||
$data['description'] = wpautop($product->get_description());
|
||||
$data['short_description'] = wpautop($product->get_short_description());
|
||||
$data['sku'] = $product->get_sku();
|
||||
$data['tags'] = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']);
|
||||
|
||||
|
||||
@@ -171,6 +171,11 @@ class TemplateOverride
|
||||
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
|
||||
'top'
|
||||
);
|
||||
add_rewrite_rule(
|
||||
'^subscribe/?$',
|
||||
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=subscribe',
|
||||
'top'
|
||||
);
|
||||
|
||||
// /order-pay/* → SPA page
|
||||
add_rewrite_rule(
|
||||
@@ -352,7 +357,6 @@ class TemplateOverride
|
||||
if ($post->ID == $spa_page_id || $post->ID == $frontpage_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if page has WooNooW structure
|
||||
$structure = get_post_meta($post->ID, '_wn_page_structure', true);
|
||||
if (!empty($structure) && !empty($structure['sections'])) {
|
||||
@@ -364,11 +368,6 @@ class TemplateOverride
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve SPA template directly for frontpage SPA routes
|
||||
* When SPA page is set as WordPress frontpage, intercept known routes
|
||||
* and serve the SPA template directly (bypasses WooCommerce templates)
|
||||
*/
|
||||
/**
|
||||
* Serve SPA template directly for frontpage SPA routes
|
||||
* When SPA page is set as WordPress frontpage, intercept known routes
|
||||
@@ -417,8 +416,8 @@ class TemplateOverride
|
||||
'/my-account', // Account page
|
||||
'/login', // Login page
|
||||
'/register', // Register page
|
||||
'/register', // Register page
|
||||
'/reset-password', // Password reset
|
||||
'/subscribe', // Subscribe page
|
||||
'/order-pay', // Order pay page
|
||||
];
|
||||
|
||||
@@ -535,6 +534,32 @@ class TemplateOverride
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a structural page with WooNooW sections
|
||||
if (is_singular('page')) {
|
||||
$page_id = get_queried_object_id();
|
||||
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||
if (!empty($structure) && !empty($structure['sections'])) {
|
||||
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||
if (file_exists($spa_template)) {
|
||||
return $spa_template;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a CPT singular with a WooNooW template
|
||||
if (is_singular() && !is_singular('page')) {
|
||||
$post_type = get_post_type();
|
||||
if ($post_type) {
|
||||
$cpt_template = get_option("wn_template_{$post_type}", null);
|
||||
if (!empty($cpt_template) && !empty($cpt_template['sections'])) {
|
||||
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||
if (file_exists($spa_template)) {
|
||||
return $spa_template;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For spa_mode = 'full', override WooCommerce pages
|
||||
if ($spa_mode === 'full') {
|
||||
// Override all WooCommerce pages
|
||||
@@ -569,23 +594,30 @@ class TemplateOverride
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine page type
|
||||
$page_type = 'shop';
|
||||
// Determine page type and route
|
||||
$data_attrs = 'data-page="shop"';
|
||||
|
||||
// Pass current request URI as initial route so router doesn't fallback to /shop
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||
$current_path = parse_url($request_uri, PHP_URL_PATH);
|
||||
$data_attrs .= ' data-initial-route="' . esc_attr($current_path) . '"';
|
||||
|
||||
if (is_product()) {
|
||||
$page_type = 'product';
|
||||
global $post;
|
||||
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
||||
$data_attrs .= ' data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
||||
} elseif (is_cart()) {
|
||||
$page_type = 'cart';
|
||||
$data_attrs = 'data-page="cart"';
|
||||
$data_attrs .= ' data-page="cart"';
|
||||
} elseif (is_checkout()) {
|
||||
$page_type = 'checkout';
|
||||
$data_attrs = 'data-page="checkout"';
|
||||
$data_attrs .= ' data-page="checkout"';
|
||||
} elseif (is_account_page()) {
|
||||
$page_type = 'account';
|
||||
$data_attrs = 'data-page="account"';
|
||||
$data_attrs .= ' data-page="account"';
|
||||
} elseif (is_singular('page')) {
|
||||
$data_attrs .= ' data-page="page"';
|
||||
} elseif (is_singular() && !is_singular('page')) {
|
||||
// CPT single item with a WooNooW template
|
||||
global $post;
|
||||
$post_type = get_post_type();
|
||||
$data_attrs .= ' data-page="cpt" data-cpt-type="' . esc_attr($post_type) . '" data-cpt-slug="' . esc_attr($post->post_name) . '"';
|
||||
}
|
||||
|
||||
// Output SPA mount point
|
||||
@@ -631,6 +663,26 @@ class TemplateOverride
|
||||
return true;
|
||||
}
|
||||
|
||||
// For structural pages (is_singular('page'))
|
||||
if (is_singular('page')) {
|
||||
$page_id = get_queried_object_id();
|
||||
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||
if (!empty($structure) && !empty($structure['sections'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For CPT singular items with a WooNooW template
|
||||
if (is_singular() && !is_singular('page')) {
|
||||
$post_type = get_post_type();
|
||||
if ($post_type) {
|
||||
$cpt_template = get_option("wn_template_{$post_type}", null);
|
||||
if (!empty($cpt_template) && !empty($cpt_template['sections'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user