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:
Dwindi Ramadhana
2026-03-04 01:14:56 +07:00
parent 7ff429502d
commit 90169b508d
46 changed files with 2337 additions and 1278 deletions

View File

@@ -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;
}