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:
@@ -381,6 +381,7 @@ class AppearanceController
|
||||
'image_position' => sanitize_text_field($data['layout']['image_position'] ?? 'left'),
|
||||
'gallery_style' => sanitize_text_field($data['layout']['gallery_style'] ?? 'thumbnails'),
|
||||
'sticky_add_to_cart' => (bool) ($data['layout']['sticky_add_to_cart'] ?? false),
|
||||
'layout_style' => sanitize_text_field($data['layout']['layout_style'] ?? 'flat'),
|
||||
],
|
||||
'elements' => [
|
||||
'breadcrumbs' => (bool) ($data['elements']['breadcrumbs'] ?? true),
|
||||
@@ -601,7 +602,11 @@ class AppearanceController
|
||||
'show_icon' => true,
|
||||
],
|
||||
],
|
||||
'product' => [],
|
||||
'product' => [
|
||||
'layout' => [
|
||||
'layout_style' => 'flat',
|
||||
],
|
||||
],
|
||||
'cart' => [],
|
||||
'checkout' => [],
|
||||
'thankyou' => [],
|
||||
|
||||
@@ -75,7 +75,7 @@ class Assets
|
||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||
'storeUrl' => self::get_spa_url(),
|
||||
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||
'onboardingCompleted' => get_option('woonoow_onboarding_completed', false),
|
||||
'onboardingCompleted' => (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1),
|
||||
]);
|
||||
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
|
||||
|
||||
@@ -201,7 +201,7 @@ class Assets
|
||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||
'storeUrl' => self::get_spa_url(),
|
||||
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||
'onboardingCompleted' => get_option('woonoow_onboarding_completed', false),
|
||||
'onboardingCompleted' => (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1),
|
||||
]);
|
||||
|
||||
// WordPress REST API settings (for media upload compatibility)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Admin;
|
||||
|
||||
/**
|
||||
@@ -9,184 +10,193 @@ namespace WooNooW\Admin;
|
||||
*
|
||||
* @package WooNooW\Admin
|
||||
*/
|
||||
class StandaloneAdmin {
|
||||
|
||||
class StandaloneAdmin
|
||||
{
|
||||
|
||||
/**
|
||||
* Initialize standalone admin handler
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
// Catch /admin requests very early (before WordPress routing)
|
||||
add_action( 'parse_request', [ __CLASS__, 'handle_admin_request' ], 1 );
|
||||
add_action('parse_request', [__CLASS__, 'handle_admin_request'], 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle /admin requests
|
||||
*/
|
||||
public static function handle_admin_request() {
|
||||
public static function handle_admin_request()
|
||||
{
|
||||
// Check if this is an /admin request
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
|
||||
|
||||
// Remove query string
|
||||
$path = strtok( $request_uri, '?' );
|
||||
|
||||
$path = strtok($request_uri, '?');
|
||||
|
||||
// Only handle exact /admin or /admin/ paths (not asset files)
|
||||
if ( $path !== '/admin' && $path !== '/admin/' ) {
|
||||
if ($path !== '/admin' && $path !== '/admin/') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// This is a standalone admin request
|
||||
self::render_standalone_admin();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render standalone admin interface
|
||||
*/
|
||||
private static function render_standalone_admin() {
|
||||
private static function render_standalone_admin()
|
||||
{
|
||||
// Enqueue WordPress media library (needed for image uploads)
|
||||
wp_enqueue_media();
|
||||
|
||||
|
||||
// Check if user is logged in and has permissions
|
||||
$is_logged_in = is_user_logged_in();
|
||||
$has_permission = $is_logged_in && current_user_can( 'manage_woocommerce' );
|
||||
$has_permission = $is_logged_in && current_user_can('manage_woocommerce');
|
||||
$is_authenticated = $is_logged_in && $has_permission;
|
||||
|
||||
|
||||
// Debug logging (only in WP_DEBUG mode)
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Get nonce for REST API
|
||||
$nonce = wp_create_nonce( 'wp_rest' );
|
||||
$rest_url = untrailingslashit( rest_url( 'woonoow/v1' ) );
|
||||
$wp_admin_url = admin_url( 'admin.php?page=woonoow' );
|
||||
|
||||
$nonce = wp_create_nonce('wp_rest');
|
||||
$rest_url = untrailingslashit(rest_url('woonoow/v1'));
|
||||
$wp_admin_url = admin_url('admin.php?page=woonoow');
|
||||
|
||||
// Get current user data if authenticated
|
||||
$current_user = null;
|
||||
if ( $is_authenticated ) {
|
||||
if ($is_authenticated) {
|
||||
$user = wp_get_current_user();
|
||||
$current_user = [
|
||||
'id' => $user->ID,
|
||||
'name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
'avatar' => get_avatar_url( $user->ID ),
|
||||
'avatar' => get_avatar_url($user->ID),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Get WooCommerce store settings
|
||||
$store_settings = self::get_store_settings();
|
||||
|
||||
|
||||
// Get asset URLs
|
||||
$plugin_url = plugins_url( '', dirname( dirname( __FILE__ ) ) );
|
||||
$plugin_url = plugins_url('', dirname(dirname(__FILE__)));
|
||||
$asset_url = $plugin_url . '/admin-spa/dist';
|
||||
|
||||
|
||||
// Cache busting
|
||||
$version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : '1.0.0';
|
||||
$version = defined('WP_DEBUG') && WP_DEBUG ? time() : '1.0.0';
|
||||
$css_url = $asset_url . '/app.css?ver=' . $version;
|
||||
$js_url = $asset_url . '/app.js?ver=' . $version;
|
||||
|
||||
|
||||
// Render HTML
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo esc_attr( get_locale() ); ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?php echo esc_html( get_option( 'blogname', 'WooNooW' ) ); ?> Admin</title>
|
||||
|
||||
<?php
|
||||
// Favicon
|
||||
$icon = get_option( 'woonoow_store_icon', '' );
|
||||
if ( ! empty( $icon ) ) {
|
||||
?>
|
||||
<link rel="icon" type="image/png" href="<?php echo esc_url( $icon ); ?>" />
|
||||
<link rel="apple-touch-icon" href="<?php echo esc_url( $icon ); ?>" />
|
||||
<?php
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo esc_attr(get_locale()); ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?php echo esc_html(get_option('blogname', 'WooNooW')); ?> Admin</title>
|
||||
|
||||
<?php
|
||||
// Favicon
|
||||
$icon = get_option('woonoow_store_icon', '');
|
||||
if (! empty($icon)) {
|
||||
?>
|
||||
<link rel="icon" type="image/png" href="<?php echo esc_url($icon); ?>" />
|
||||
<link rel="apple-touch-icon" href="<?php echo esc_url($icon); ?>" />
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Print WordPress media library styles (complete set for proper modal)
|
||||
wp_print_styles('media-views');
|
||||
wp_print_styles('imgareaselect');
|
||||
wp_print_styles('buttons');
|
||||
wp_print_styles('dashicons');
|
||||
wp_print_styles('wp-admin');
|
||||
wp_print_styles('common');
|
||||
?>
|
||||
|
||||
<!-- WooNooW Assets -->
|
||||
<link rel="stylesheet" href="<?php echo esc_url($css_url); ?>">
|
||||
</head>
|
||||
|
||||
<body class="woonoow-standalone">
|
||||
<div id="woonoow-admin-app"></div>
|
||||
|
||||
<script>
|
||||
// Minimal config - no WordPress bloat
|
||||
window.WNW_CONFIG = {
|
||||
restUrl: <?php echo wp_json_encode($rest_url); ?>,
|
||||
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||
standaloneMode: true,
|
||||
wpAdminUrl: <?php echo wp_json_encode($wp_admin_url); ?>,
|
||||
isAuthenticated: <?php echo $is_authenticated ? 'true' : 'false'; ?>,
|
||||
currentUser: <?php echo wp_json_encode($current_user); ?>,
|
||||
locale: <?php echo wp_json_encode(get_locale()); ?>,
|
||||
siteUrl: <?php echo wp_json_encode(home_url()); ?>,
|
||||
siteName: <?php echo wp_json_encode(get_bloginfo('name')); ?>,
|
||||
storeUrl: <?php echo wp_json_encode(self::get_spa_url()); ?>,
|
||||
customerSpaEnabled: <?php echo get_option('woonoow_customer_spa_enabled', false) ? 'true' : 'false'; ?>,
|
||||
onboardingCompleted: <?php echo (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1) ? 'true' : 'false'; ?>
|
||||
};
|
||||
|
||||
// Also set WNW_API for API compatibility
|
||||
window.WNW_API = {
|
||||
root: <?php echo wp_json_encode($rest_url); ?>,
|
||||
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||
isDev: <?php echo (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false'; ?>
|
||||
};
|
||||
|
||||
// WooCommerce store settings (currency, formatting, etc.)
|
||||
window.WNW_STORE = <?php echo wp_json_encode($store_settings); ?>;
|
||||
|
||||
// Navigation tree (single source of truth from PHP)
|
||||
window.WNW_NAV_TREE = <?php echo wp_json_encode(\WooNooW\Compat\NavigationRegistry::get_frontend_nav_tree()); ?>;
|
||||
|
||||
// WordPress REST API settings (for media upload compatibility)
|
||||
window.wpApiSettings = {
|
||||
root: <?php echo wp_json_encode(untrailingslashit(rest_url())); ?>,
|
||||
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||
versionString: 'wp/v2/'
|
||||
};
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// Print WordPress media library scripts (needed for wp.media)
|
||||
wp_print_scripts('media-editor');
|
||||
wp_print_scripts('media-audiovideo');
|
||||
|
||||
// Print media templates (required for media modal to work)
|
||||
wp_print_media_templates();
|
||||
?>
|
||||
|
||||
<script type="module" src="<?php echo esc_url($js_url); ?>"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Print WordPress media library styles (complete set for proper modal)
|
||||
wp_print_styles( 'media-views' );
|
||||
wp_print_styles( 'imgareaselect' );
|
||||
wp_print_styles( 'buttons' );
|
||||
wp_print_styles( 'dashicons' );
|
||||
wp_print_styles( 'wp-admin' );
|
||||
wp_print_styles( 'common' );
|
||||
?>
|
||||
|
||||
<!-- WooNooW Assets -->
|
||||
<link rel="stylesheet" href="<?php echo esc_url( $css_url ); ?>">
|
||||
</head>
|
||||
<body class="woonoow-standalone">
|
||||
<div id="woonoow-admin-app"></div>
|
||||
|
||||
<script>
|
||||
// Minimal config - no WordPress bloat
|
||||
window.WNW_CONFIG = {
|
||||
restUrl: <?php echo wp_json_encode( $rest_url ); ?>,
|
||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
||||
standaloneMode: true,
|
||||
wpAdminUrl: <?php echo wp_json_encode( $wp_admin_url ); ?>,
|
||||
isAuthenticated: <?php echo $is_authenticated ? 'true' : 'false'; ?>,
|
||||
currentUser: <?php echo wp_json_encode( $current_user ); ?>,
|
||||
locale: <?php echo wp_json_encode( get_locale() ); ?>,
|
||||
siteUrl: <?php echo wp_json_encode( home_url() ); ?>,
|
||||
siteName: <?php echo wp_json_encode( get_bloginfo( 'name' ) ); ?>,
|
||||
storeUrl: <?php echo wp_json_encode( self::get_spa_url() ); ?>,
|
||||
customerSpaEnabled: <?php echo get_option( 'woonoow_customer_spa_enabled', false ) ? 'true' : 'false'; ?>
|
||||
};
|
||||
|
||||
// Also set WNW_API for API compatibility
|
||||
window.WNW_API = {
|
||||
root: <?php echo wp_json_encode( $rest_url ); ?>,
|
||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
||||
isDev: <?php echo ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'true' : 'false'; ?>
|
||||
};
|
||||
|
||||
// WooCommerce store settings (currency, formatting, etc.)
|
||||
window.WNW_STORE = <?php echo wp_json_encode( $store_settings ); ?>;
|
||||
|
||||
// Navigation tree (single source of truth from PHP)
|
||||
window.WNW_NAV_TREE = <?php echo wp_json_encode( \WooNooW\Compat\NavigationRegistry::get_frontend_nav_tree() ); ?>;
|
||||
|
||||
// WordPress REST API settings (for media upload compatibility)
|
||||
window.wpApiSettings = {
|
||||
root: <?php echo wp_json_encode( untrailingslashit( rest_url() ) ); ?>,
|
||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
||||
versionString: 'wp/v2/'
|
||||
};
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// Print WordPress media library scripts (needed for wp.media)
|
||||
wp_print_scripts( 'media-editor' );
|
||||
wp_print_scripts( 'media-audiovideo' );
|
||||
|
||||
// Print media templates (required for media modal to work)
|
||||
wp_print_media_templates();
|
||||
?>
|
||||
|
||||
<script type="module" src="<?php echo esc_url( $js_url ); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get WooCommerce store settings for frontend
|
||||
*
|
||||
* @return array Store settings (currency, decimals, separators, etc.)
|
||||
*/
|
||||
private static function get_store_settings(): array {
|
||||
private static function get_store_settings(): array
|
||||
{
|
||||
// Get WooCommerce settings with fallbacks
|
||||
$currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD';
|
||||
$currency_sym = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol( $currency ) : '$';
|
||||
$decimals = function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2;
|
||||
$thousand_sep = function_exists( 'wc_get_price_thousand_separator' ) ? wc_get_price_thousand_separator() : ',';
|
||||
$decimal_sep = function_exists( 'wc_get_price_decimal_separator' ) ? wc_get_price_decimal_separator() : '.';
|
||||
$currency_pos = get_option( 'woocommerce_currency_pos', 'left' );
|
||||
|
||||
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';
|
||||
$currency_sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol($currency) : '$';
|
||||
$decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
|
||||
$thousand_sep = function_exists('wc_get_price_thousand_separator') ? wc_get_price_thousand_separator() : ',';
|
||||
$decimal_sep = function_exists('wc_get_price_decimal_separator') ? wc_get_price_decimal_separator() : '.';
|
||||
$currency_pos = get_option('woocommerce_currency_pos', 'left');
|
||||
|
||||
return [
|
||||
'currency' => $currency,
|
||||
'currency_symbol' => $currency_sym,
|
||||
@@ -200,17 +210,17 @@ class StandaloneAdmin {
|
||||
/** Get the SPA page URL from appearance settings (dynamic slug) */
|
||||
private static function get_spa_url(): string
|
||||
{
|
||||
$appearance_settings = get_option( 'woonoow_appearance_settings', [] );
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
||||
|
||||
if ( $spa_page_id ) {
|
||||
$spa_url = get_permalink( $spa_page_id );
|
||||
if ( $spa_url ) {
|
||||
return trailingslashit( $spa_url );
|
||||
|
||||
if ($spa_page_id) {
|
||||
$spa_url = get_permalink($spa_page_id);
|
||||
if ($spa_url) {
|
||||
return trailingslashit($spa_url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to /store/ if no SPA page configured
|
||||
return home_url( '/store/' );
|
||||
return home_url('/store/');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user