feat: Dynamic SPA slug, field label storage, and SPA frontpage support (WIP)

This commit is contained in:
Dwindi Ramadhana
2026-01-10 00:50:32 +07:00
parent d3ec580ec8
commit 3357fbfcf1
20 changed files with 1317 additions and 465 deletions

View File

@@ -86,25 +86,39 @@ class AddressController {
// Generate new ID
$new_id = empty($addresses) ? 1 : max(array_column($addresses, 'id')) + 1;
// Prepare address data
// Standard address fields
$standard_fields = ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone'];
$reserved_fields = ['id', 'label', 'type', 'is_default'];
// Prepare address data with standard fields
$address = [
'id' => $new_id,
'label' => sanitize_text_field($request->get_param('label')),
'type' => sanitize_text_field($request->get_param('type')), // 'billing', 'shipping', or 'both'
'first_name' => sanitize_text_field($request->get_param('first_name')),
'last_name' => sanitize_text_field($request->get_param('last_name')),
'company' => sanitize_text_field($request->get_param('company')),
'address_1' => sanitize_text_field($request->get_param('address_1')),
'address_2' => sanitize_text_field($request->get_param('address_2')),
'city' => sanitize_text_field($request->get_param('city')),
'state' => sanitize_text_field($request->get_param('state')),
'postcode' => sanitize_text_field($request->get_param('postcode')),
'country' => sanitize_text_field($request->get_param('country')),
'email' => sanitize_email($request->get_param('email')),
'phone' => sanitize_text_field($request->get_param('phone')),
'is_default' => (bool) $request->get_param('is_default'),
];
// Add standard fields
foreach ($standard_fields as $field) {
$value = $request->get_param($field);
if ($field === 'email') {
$address[$field] = sanitize_email($value);
} else {
$address[$field] = sanitize_text_field($value);
}
}
// Add any custom fields (like destination_id from Rajaongkir)
$all_params = $request->get_json_params();
if (is_array($all_params)) {
foreach ($all_params as $key => $value) {
if (!in_array($key, $standard_fields) && !in_array($key, $reserved_fields)) {
// Store custom field
$address[$key] = is_string($value) ? sanitize_text_field($value) : $value;
}
}
}
// If this is set as default, unset other defaults of the same type
if ($address['is_default']) {
foreach ($addresses as &$addr) {
@@ -138,22 +152,36 @@ class AddressController {
if ($addr['id'] === $address_id) {
$found = true;
// Update fields
$addr['label'] = sanitize_text_field($request->get_param('label'));
$addr['type'] = sanitize_text_field($request->get_param('type'));
$addr['first_name'] = sanitize_text_field($request->get_param('first_name'));
$addr['last_name'] = sanitize_text_field($request->get_param('last_name'));
$addr['company'] = sanitize_text_field($request->get_param('company'));
$addr['address_1'] = sanitize_text_field($request->get_param('address_1'));
$addr['address_2'] = sanitize_text_field($request->get_param('address_2'));
$addr['city'] = sanitize_text_field($request->get_param('city'));
$addr['state'] = sanitize_text_field($request->get_param('state'));
$addr['postcode'] = sanitize_text_field($request->get_param('postcode'));
$addr['country'] = sanitize_text_field($request->get_param('country'));
$addr['email'] = sanitize_email($request->get_param('email'));
$addr['phone'] = sanitize_text_field($request->get_param('phone'));
// Standard address fields
$standard_fields = ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone'];
$reserved_fields = ['id', 'label', 'type', 'is_default'];
// Update standard meta fields
$addr['label'] = sanitize_text_field($request->get_param('label'));
$addr['type'] = sanitize_text_field($request->get_param('type'));
$addr['is_default'] = (bool) $request->get_param('is_default');
// Update standard fields
foreach ($standard_fields as $field) {
$value = $request->get_param($field);
if ($field === 'email') {
$addr[$field] = sanitize_email($value);
} else {
$addr[$field] = sanitize_text_field($value);
}
}
// Update any custom fields (like destination_id from Rajaongkir)
$all_params = $request->get_json_params();
if (is_array($all_params)) {
foreach ($all_params as $key => $value) {
if (!in_array($key, $standard_fields) && !in_array($key, $reserved_fields)) {
// Store/update custom field
$addr[$key] = is_string($value) ? sanitize_text_field($value) : $value;
}
}
}
// If this is set as default, unset other defaults of the same type
if ($addr['is_default']) {
foreach ($addresses as &$other_addr) {

View File

@@ -197,7 +197,13 @@ class Assets {
// Determine SPA base path for BrowserRouter
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_page = $spa_page_id ? get_post($spa_page_id) : null;
$base_path = $spa_page ? '/' . $spa_page->post_name : '/store';
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
$is_spa_frontpage = $frontpage_id && $spa_page_id && $frontpage_id === (int) $spa_page_id;
// If SPA is frontpage, base path is /, otherwise use page slug
$base_path = $is_spa_frontpage ? '' : ($spa_page ? '/' . $spa_page->post_name : '/store');
// Check if BrowserRouter is enabled (default: true for SEO)
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
@@ -249,6 +255,16 @@ class Assets {
private static function should_load_assets() {
global $post;
// Check if we're serving SPA directly (set by serve_spa_for_frontpage_routes)
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;
}
// First check: Is this a designated SPA page?
if (self::is_spa_page()) {
return true;
@@ -366,6 +382,51 @@ class Assets {
return false;
}
/**
* Check if current request is a frontpage SPA route
* Used to detect SPA routes by URL when SPA page is set as frontpage
*/
private static function is_frontpage_spa_route() {
// Get SPA settings
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// Only run in full SPA mode
if ($spa_mode !== 'full' || !$spa_page_id) {
return false;
}
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
if (!$frontpage_id || $frontpage_id !== (int) $spa_page_id) {
return false;
}
// Get the current request path
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($request_uri, PHP_URL_PATH);
$path = '/' . trim($path, '/');
// Define SPA routes
$spa_routes = ['/', '/shop', '/cart', '/checkout', '/my-account', '/login', '/register', '/reset-password'];
// Check exact matches
if (in_array($path, $spa_routes)) {
return true;
}
// Check path prefixes
$prefix_routes = ['/shop/', '/my-account/', '/product/'];
foreach ($prefix_routes as $prefix) {
if (strpos($path, $prefix) === 0) {
return true;
}
}
return false;
}
/**
* Dequeue conflicting scripts when SPA is active
*/

View File

@@ -35,6 +35,9 @@ class TemplateOverride
// Redirect WooCommerce pages to SPA routes early (before template loads)
add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5);
// Serve SPA directly for frontpage routes (priority 1 = very early, before WC)
add_action('template_redirect', [__CLASS__, 'serve_spa_for_frontpage_routes'], 1);
// Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
// This ensures we process add-to-cart before WooCommerce does
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
@@ -68,7 +71,7 @@ class TemplateOverride
/**
* Register rewrite rules for BrowserRouter SEO
* Catches all /store/* routes and serves the SPA page
* Catches all SPA routes and serves the SPA page
*/
public static function register_spa_rewrite_rules()
{
@@ -89,13 +92,82 @@ class TemplateOverride
$spa_slug = $spa_page->post_name;
// Rewrite /store/anything to serve the SPA page
// React Router handles the path after that
add_rewrite_rule(
'^' . preg_quote($spa_slug, '/') . '/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=$matches[1]',
'top'
);
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
$is_spa_frontpage = $frontpage_id && $frontpage_id === (int) $spa_page_id;
if ($is_spa_frontpage) {
// When SPA is frontpage, add root-level routes
// /shop, /shop/* → SPA page
add_rewrite_rule(
'^shop/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=shop',
'top'
);
add_rewrite_rule(
'^shop/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=shop/$matches[1]',
'top'
);
// /product/* → SPA page
add_rewrite_rule(
'^product/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=product/$matches[1]',
'top'
);
// /cart → SPA page
add_rewrite_rule(
'^cart/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=cart',
'top'
);
// /checkout → SPA page
add_rewrite_rule(
'^checkout/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout',
'top'
);
// /my-account, /my-account/* → SPA page
add_rewrite_rule(
'^my-account/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=my-account',
'top'
);
add_rewrite_rule(
'^my-account/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=my-account/$matches[1]',
'top'
);
// /login, /register, /reset-password → SPA page
add_rewrite_rule(
'^login/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=login',
'top'
);
add_rewrite_rule(
'^register/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=register',
'top'
);
add_rewrite_rule(
'^reset-password/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
'top'
);
} else {
// Rewrite /slug/anything to serve the SPA page
// React Router handles the path after that
add_rewrite_rule(
'^' . preg_quote($spa_slug, '/') . '/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=$matches[1]',
'top'
);
}
// Register query var for the SPA path
add_filter('query_vars', function($vars) {
@@ -174,6 +246,12 @@ class TemplateOverride
return; // No SPA page configured
}
// Skip if SPA is set as frontpage (serve_spa_for_frontpage_routes handles it)
$frontpage_id = (int) get_option('page_on_front');
if ($frontpage_id && $frontpage_id === (int) $spa_page_id) {
return;
}
// Already on SPA page, don't redirect
global $post;
if ($post && $post->ID == $spa_page_id) {
@@ -224,6 +302,88 @@ 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)
*/
public static function serve_spa_for_frontpage_routes()
{
// Get SPA settings
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// Only run in full SPA mode
if ($spa_mode !== 'full' || !$spa_page_id) {
return;
}
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
if (!$frontpage_id || $frontpage_id !== (int) $spa_page_id) {
return; // SPA is not frontpage, let normal routing handle it
}
// Get the current request path
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($request_uri, PHP_URL_PATH);
$path = '/' . trim($path, '/');
// Define SPA routes that should be intercepted when SPA is frontpage
$spa_routes = [
'/', // Frontpage itself
'/shop', // Shop page
'/cart', // Cart page
'/checkout', // Checkout page
'/my-account', // Account page
'/login', // Login page
'/register', // Register page
'/reset-password', // Password reset
];
// Check for exact matches or path prefixes
$should_serve_spa = false;
// Check exact matches
if (in_array($path, $spa_routes)) {
$should_serve_spa = true;
}
// Check path prefixes (for sub-routes)
$prefix_routes = ['/shop/', '/my-account/', '/product/'];
foreach ($prefix_routes as $prefix) {
if (strpos($path, $prefix) === 0) {
$should_serve_spa = true;
break;
}
}
// Not a SPA route
if (!$should_serve_spa) {
return;
}
// Prevent caching for dynamic SPA content
nocache_headers();
// Load the SPA template directly and exit
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
// Set up minimal WordPress environment for the template
status_header(200);
// Define constant to tell Assets to load unconditionally
if (!defined('WOONOOW_SERVE_SPA')) {
define('WOONOOW_SERVE_SPA', true);
}
// Include the SPA template
include $spa_template;
exit;
}
}
/**
* Disable canonical redirects for SPA routes
* This prevents WordPress from redirecting /product/slug URLs
@@ -406,17 +566,25 @@ class TemplateOverride
private static function is_spa_page()
{
global $post;
if (!$post) {
return false;
}
// Get SPA settings from appearance
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// Only return true if spa_mode is 'full' AND we're on the SPA page
if ($spa_mode === 'full' && $spa_page_id && $post->ID == $spa_page_id) {
// Only check if spa_mode is 'full' and SPA page is configured
if ($spa_mode !== 'full' || !$spa_page_id) {
return false;
}
// Check if current page is the SPA page
if ($post && $post->ID == $spa_page_id) {
return true;
}
// Check if SPA page is set as WordPress frontpage and we're on frontpage
$frontpage_id = (int) get_option('page_on_front');
if ($frontpage_id && $frontpage_id === (int) $spa_page_id && is_front_page()) {
return true;
}