Files
WooNooW/includes/Admin/StandaloneAdmin.php
Dwindi Ramadhana 90169b508d 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
2026-03-04 01:14:56 +07:00

227 lines
7.0 KiB
PHP

<?php
namespace WooNooW\Admin;
/**
* Standalone Admin Handler
*
* Handles /admin requests without requiring .htaccess modifications.
* Uses WordPress template_redirect hook to catch requests early.
*
* @package WooNooW\Admin
*/
class StandaloneAdmin
{
/**
* Initialize standalone admin handler
*/
public static function init()
{
// Catch /admin requests very early (before WordPress routing)
add_action('parse_request', [__CLASS__, 'handle_admin_request'], 1);
}
/**
* Handle /admin requests
*/
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, '?');
// Only handle exact /admin or /admin/ paths (not asset files)
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()
{
// 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');
$is_authenticated = $is_logged_in && $has_permission;
// Debug logging (only in WP_DEBUG mode)
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');
// Get current user data if authenticated
$current_user = null;
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),
];
}
// Get WooCommerce store settings
$store_settings = self::get_store_settings();
// Get asset URLs
$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';
$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
}
?>
<?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
}
/**
* Get WooCommerce store settings for frontend
*
* @return array Store settings (currency, decimals, separators, etc.)
*/
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');
return [
'currency' => $currency,
'currency_symbol' => $currency_sym,
'decimals' => (int) $decimals,
'thousand_sep' => (string) $thousand_sep,
'decimal_sep' => (string) $decimal_sep,
'currency_pos' => (string) $currency_pos,
];
}
/** Get the SPA page URL from appearance settings (dynamic slug) */
private static function get_spa_url(): string
{
$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);
}
}
// Fallback to /store/ if no SPA page configured
return home_url('/store/');
}
}