Files
WooNooW/includes/Admin/AppearanceController.php
Dwindi Ramadhana 012effd11d feat: Add dedicated SPA page selection (WooCommerce-style)
Problem: Shortcode 'island' architecture is fragile and theme-dependent
- SPA div buried deep in theme structure (body > div.wp-site-blocks > main > div#app)
- Theme and plugins can intervene at any level
- Different themes have different structures
- Breaks easily with theme changes

Solution: Dedicated page-based SPA system (like WooCommerce)
- Add page selection in Appearance > General settings
- Store page IDs for Shop, Cart, Checkout, Account
- Full-body SPA rendering on designated pages
- No theme interference

Changes:
- AppearanceController.php:
  * Added spa_pages field to general settings
  * Stores page IDs for each SPA type (shop/cart/checkout/account)

- TemplateOverride.php:
  * Added is_spa_page() method to check designated pages
  * Use blank template for designated pages (priority over legacy)
  * Remove theme elements for designated pages

- Assets.php:
  * Added is_spa_page() check before mode/shortcode checks
  * Load assets on designated pages regardless of mode

Architecture:
- Designated pages render directly to <body>
- No theme wrapper/structure interference
- Clean full-page SPA experience
- Works with ANY theme consistently

Next: Add UI in admin-spa General tab for page selection
2025-12-30 19:42:16 +07:00

469 lines
21 KiB
PHP

<?php
namespace WooNooW\Admin;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class AppearanceController {
const OPTION_KEY = 'woonoow_appearance_settings';
const API_NAMESPACE = 'woonoow/v1';
public static function init() {
add_action('rest_api_init', [__CLASS__, 'register_routes']);
}
public static function register_routes() {
// Get all settings (public access for frontend)
register_rest_route(self::API_NAMESPACE, '/appearance/settings', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_settings'],
'permission_callback' => '__return_true',
]);
// Save general settings
register_rest_route(self::API_NAMESPACE, '/appearance/general', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_general'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Save header settings
register_rest_route(self::API_NAMESPACE, '/appearance/header', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_header'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Save footer settings
register_rest_route(self::API_NAMESPACE, '/appearance/footer', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_footer'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Save page-specific settings
register_rest_route(self::API_NAMESPACE, '/appearance/pages/(?P<page>[a-zA-Z0-9_-]+)', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_page_settings'],
'permission_callback' => [__CLASS__, 'check_permission'],
'args' => [
'page' => [
'required' => true,
'type' => 'string',
'enum' => ['shop', 'product', 'cart', 'checkout', 'thankyou', 'account'],
],
],
]);
}
public static function check_permission() {
return current_user_can('manage_woocommerce');
}
/**
* Get all appearance settings
*/
public static function get_settings(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
return new WP_REST_Response([
'success' => true,
'data' => $settings,
], 200);
}
/**
* Save general settings
*/
public static function save_general(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$general_data = [
'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
'spa_pages' => [
'shop' => absint($request->get_param('spaPages')['shop'] ?? 0),
'cart' => absint($request->get_param('spaPages')['cart'] ?? 0),
'checkout' => absint($request->get_param('spaPages')['checkout'] ?? 0),
'account' => absint($request->get_param('spaPages')['account'] ?? 0),
],
'toast_position' => sanitize_text_field($request->get_param('toastPosition') ?? 'top-right'),
'typography' => [
'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'),
'predefined_pair' => sanitize_text_field($request->get_param('typography')['predefined_pair'] ?? 'modern'),
'custom' => [
'heading' => sanitize_text_field($request->get_param('typography')['custom']['heading'] ?? ''),
'body' => sanitize_text_field($request->get_param('typography')['custom']['body'] ?? ''),
],
'scale' => floatval($request->get_param('typography')['scale'] ?? 1.0),
],
'colors' => [
'primary' => sanitize_hex_color($request->get_param('colors')['primary'] ?? '#1a1a1a'),
'secondary' => sanitize_hex_color($request->get_param('colors')['secondary'] ?? '#6b7280'),
'accent' => sanitize_hex_color($request->get_param('colors')['accent'] ?? '#3b82f6'),
'text' => sanitize_hex_color($request->get_param('colors')['text'] ?? '#111827'),
'background' => sanitize_hex_color($request->get_param('colors')['background'] ?? '#ffffff'),
],
];
$settings['general'] = $general_data;
update_option(self::OPTION_KEY, $settings);
return new WP_REST_Response([
'success' => true,
'message' => 'General settings saved successfully',
'data' => $general_data,
], 200);
}
/**
* Save header settings
*/
public static function save_header(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$header_data = [
'style' => sanitize_text_field($request->get_param('style')),
'sticky' => (bool) $request->get_param('sticky'),
'height' => sanitize_text_field($request->get_param('height')),
'mobile_menu' => sanitize_text_field($request->get_param('mobileMenu')),
'mobile_logo' => sanitize_text_field($request->get_param('mobileLogo')),
'logo_width' => sanitize_text_field($request->get_param('logoWidth') ?? 'auto'),
'logo_height' => sanitize_text_field($request->get_param('logoHeight') ?? '40px'),
'elements' => [
'logo' => (bool) ($request->get_param('elements')['logo'] ?? true),
'navigation' => (bool) ($request->get_param('elements')['navigation'] ?? true),
'search' => (bool) ($request->get_param('elements')['search'] ?? true),
'account' => (bool) ($request->get_param('elements')['account'] ?? true),
'cart' => (bool) ($request->get_param('elements')['cart'] ?? true),
'wishlist' => (bool) ($request->get_param('elements')['wishlist'] ?? false),
],
];
$settings['header'] = $header_data;
update_option(self::OPTION_KEY, $settings);
return new WP_REST_Response([
'success' => true,
'message' => 'Header settings saved successfully',
'data' => $header_data,
], 200);
}
/**
* Save footer settings
*/
public static function save_footer(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$social_links = $request->get_param('socialLinks') ?? [];
$sanitized_links = [];
foreach ($social_links as $link) {
$sanitized_links[] = [
'id' => sanitize_text_field($link['id'] ?? ''),
'platform' => sanitize_text_field($link['platform'] ?? ''),
'url' => esc_url_raw($link['url'] ?? ''),
];
}
// Sanitize contact data
$contact_data = $request->get_param('contactData');
$sanitized_contact = [
'email' => sanitize_email($contact_data['email'] ?? ''),
'phone' => sanitize_text_field($contact_data['phone'] ?? ''),
'address' => sanitize_textarea_field($contact_data['address'] ?? ''),
'show_email' => (bool) ($contact_data['show_email'] ?? true),
'show_phone' => (bool) ($contact_data['show_phone'] ?? true),
'show_address' => (bool) ($contact_data['show_address'] ?? true),
];
// Sanitize labels
$labels = $request->get_param('labels');
$sanitized_labels = [
'contact_title' => sanitize_text_field($labels['contact_title'] ?? 'Contact'),
'menu_title' => sanitize_text_field($labels['menu_title'] ?? 'Quick Links'),
'social_title' => sanitize_text_field($labels['social_title'] ?? 'Follow Us'),
'newsletter_title' => sanitize_text_field($labels['newsletter_title'] ?? 'Newsletter'),
'newsletter_description' => sanitize_text_field($labels['newsletter_description'] ?? 'Subscribe to get updates'),
];
// Sanitize custom sections
$sections = $request->get_param('sections') ?? [];
$sanitized_sections = [];
foreach ($sections as $section) {
$sanitized_sections[] = [
'id' => sanitize_text_field($section['id']),
'title' => sanitize_text_field($section['title']),
'type' => sanitize_text_field($section['type']),
'content' => wp_kses_post($section['content'] ?? ''),
'visible' => (bool) ($section['visible'] ?? true),
];
}
$footer_data = [
'columns' => sanitize_text_field($request->get_param('columns')),
'style' => sanitize_text_field($request->get_param('style')),
'copyright_text' => wp_kses_post($request->get_param('copyrightText')),
'elements' => [
'newsletter' => (bool) ($request->get_param('elements')['newsletter'] ?? true),
'social' => (bool) ($request->get_param('elements')['social'] ?? true),
'payment' => (bool) ($request->get_param('elements')['payment'] ?? true),
'copyright' => (bool) ($request->get_param('elements')['copyright'] ?? true),
'menu' => (bool) ($request->get_param('elements')['menu'] ?? true),
'contact' => (bool) ($request->get_param('elements')['contact'] ?? true),
],
'social_links' => $sanitized_links,
'contact_data' => $sanitized_contact,
'labels' => $sanitized_labels,
'sections' => $sanitized_sections,
];
$settings['footer'] = $footer_data;
update_option(self::OPTION_KEY, $settings);
return new WP_REST_Response([
'success' => true,
'message' => 'Footer settings saved successfully',
'data' => $footer_data,
], 200);
}
/**
* Save page-specific settings
*/
public static function save_page_settings(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$page = $request->get_param('page');
// Get all parameters from request
$page_data = $request->get_json_params();
// Sanitize based on page type
$sanitized_data = self::sanitize_page_data($page, $page_data);
$settings['pages'][$page] = $sanitized_data;
update_option(self::OPTION_KEY, $settings);
return new WP_REST_Response([
'success' => true,
'message' => ucfirst($page) . ' page settings saved successfully',
'data' => $sanitized_data,
], 200);
}
/**
* Sanitize page-specific data
*/
private static function sanitize_page_data($page, $data) {
$sanitized = [];
switch ($page) {
case 'shop':
$sanitized = [
'layout' => [
'grid_columns' => sanitize_text_field($data['layout']['grid_columns'] ?? '3'),
'grid_style' => sanitize_text_field($data['layout']['grid_style'] ?? 'standard'),
'card_style' => sanitize_text_field($data['layout']['card_style'] ?? 'card'),
'aspect_ratio' => sanitize_text_field($data['layout']['aspect_ratio'] ?? 'square'),
'card_text_align' => sanitize_text_field($data['layout']['card_text_align'] ?? 'left'),
],
'elements' => [
'category_filter' => (bool) ($data['elements']['category_filter'] ?? true),
'search_bar' => (bool) ($data['elements']['search_bar'] ?? true),
'sort_dropdown' => (bool) ($data['elements']['sort_dropdown'] ?? true),
'sale_badges' => (bool) ($data['elements']['sale_badges'] ?? true),
'quick_view' => (bool) ($data['elements']['quick_view'] ?? false),
],
'sale_badge' => [
'color' => sanitize_hex_color($data['sale_badge']['color'] ?? '#ef4444'),
],
'add_to_cart' => [
'position' => sanitize_text_field($data['add_to_cart']['position'] ?? 'below'),
'style' => sanitize_text_field($data['add_to_cart']['style'] ?? 'solid'),
'show_icon' => (bool) ($data['add_to_cart']['show_icon'] ?? true),
],
];
break;
case 'product':
$sanitized = [
'layout' => [
'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),
],
'elements' => [
'breadcrumbs' => (bool) ($data['elements']['breadcrumbs'] ?? true),
'related_products' => (bool) ($data['elements']['related_products'] ?? true),
'reviews' => (bool) ($data['elements']['reviews'] ?? true),
'share_buttons' => (bool) ($data['elements']['share_buttons'] ?? false),
'product_meta' => (bool) ($data['elements']['product_meta'] ?? true),
],
'related_products' => [
'title' => sanitize_text_field($data['related_products']['title'] ?? 'You May Also Like'),
],
'reviews' => [
'placement' => sanitize_text_field($data['reviews']['placement'] ?? 'product_page'),
'hide_if_empty' => (bool) ($data['reviews']['hide_if_empty'] ?? true),
],
];
break;
case 'cart':
$sanitized = [
'layout' => [
'style' => sanitize_text_field($data['layout']['style'] ?? 'fullwidth'),
'summary_position' => sanitize_text_field($data['layout']['summary_position'] ?? 'right'),
],
'elements' => [
'product_images' => (bool) ($data['elements']['product_images'] ?? true),
'continue_shopping_button' => (bool) ($data['elements']['continue_shopping_button'] ?? true),
'coupon_field' => (bool) ($data['elements']['coupon_field'] ?? true),
'shipping_calculator' => (bool) ($data['elements']['shipping_calculator'] ?? false),
],
];
break;
case 'checkout':
$sanitized = [
'layout' => [
'style' => sanitize_text_field($data['layout']['style'] ?? 'two-column'),
'order_summary' => sanitize_text_field($data['layout']['order_summary'] ?? 'sidebar'),
'header_visibility' => sanitize_text_field($data['layout']['header_visibility'] ?? 'minimal'),
'footer_visibility' => sanitize_text_field($data['layout']['footer_visibility'] ?? 'minimal'),
'background_color' => sanitize_hex_color($data['layout']['background_color'] ?? '#f9fafb'),
],
'elements' => [
'order_notes' => (bool) ($data['elements']['order_notes'] ?? true),
'coupon_field' => (bool) ($data['elements']['coupon_field'] ?? true),
'shipping_options' => (bool) ($data['elements']['shipping_options'] ?? true),
'payment_icons' => (bool) ($data['elements']['payment_icons'] ?? true),
],
];
break;
case 'thankyou':
$sanitized = [
'template' => sanitize_text_field($data['template'] ?? 'basic'),
'header_visibility' => sanitize_text_field($data['header_visibility'] ?? 'show'),
'footer_visibility' => sanitize_text_field($data['footer_visibility'] ?? 'minimal'),
'background_color' => sanitize_hex_color($data['background_color'] ?? '#f9fafb'),
'custom_message' => wp_kses_post($data['custom_message'] ?? ''),
'elements' => [
'order_details' => (bool) ($data['elements']['order_details'] ?? true),
'continue_shopping_button' => (bool) ($data['elements']['continue_shopping_button'] ?? true),
'related_products' => (bool) ($data['elements']['related_products'] ?? false),
],
];
break;
case 'account':
$sanitized = [
'layout' => [
'navigation_style' => sanitize_text_field($data['layout']['navigation_style'] ?? 'sidebar'),
],
'elements' => [
'dashboard' => (bool) ($data['elements']['dashboard'] ?? true),
'orders' => (bool) ($data['elements']['orders'] ?? true),
'downloads' => (bool) ($data['elements']['downloads'] ?? false),
'addresses' => (bool) ($data['elements']['addresses'] ?? true),
'account_details' => (bool) ($data['elements']['account_details'] ?? true),
],
];
break;
}
return $sanitized;
}
/**
* Get default settings structure
*/
public static function get_default_settings() {
return [
'general' => [
'spa_mode' => 'full',
'spa_pages' => [
'shop' => 0,
'cart' => 0,
'checkout' => 0,
'account' => 0,
],
'toast_position' => 'top-right',
'typography' => [
'mode' => 'predefined',
'predefined_pair' => 'modern',
'custom' => [
'heading' => '',
'body' => '',
],
'scale' => 1.0,
],
'colors' => [
'primary' => '#1a1a1a',
'secondary' => '#6b7280',
'accent' => '#3b82f6',
'text' => '#111827',
'background' => '#ffffff',
],
],
'header' => [
'style' => 'classic',
'sticky' => true,
'height' => 'normal',
'mobile_menu' => 'hamburger',
'mobile_logo' => 'left',
'elements' => [
'logo' => true,
'navigation' => true,
'search' => true,
'account' => true,
'cart' => true,
'wishlist' => false,
],
],
'footer' => [
'columns' => '4',
'style' => 'detailed',
'copyright_text' => '© 2024 WooNooW. All rights reserved.',
'elements' => [
'newsletter' => true,
'social' => true,
'payment' => true,
'copyright' => true,
'menu' => true,
'contact' => true,
],
'social_links' => [],
],
'pages' => [
'shop' => [
'layout' => [
'grid_columns' => '3',
'card_style' => 'card',
'aspect_ratio' => 'square',
],
'elements' => [
'category_filter' => true,
'search_bar' => true,
'sort_dropdown' => true,
'sale_badges' => true,
'quick_view' => false,
],
'add_to_cart' => [
'position' => 'below',
'style' => 'solid',
'show_icon' => true,
],
],
'product' => [],
'cart' => [],
'checkout' => [],
'thankyou' => [],
'account' => [],
],
];
}
}