Fix button roundtrip in editor, alignment persistence, and test email rendering

This commit is contained in:
Dwindi Ramadhana
2026-01-17 13:10:50 +07:00
parent 0e9ace902d
commit 6d2136d3b5
61 changed files with 8287 additions and 866 deletions

View File

@@ -42,6 +42,13 @@ class AppearanceController {
'callback' => [__CLASS__, 'save_footer'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Save menu settings
register_rest_route(self::API_NAMESPACE, '/appearance/menus', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_menus'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
// Save page-specific settings
register_rest_route(self::API_NAMESPACE, '/appearance/pages/(?P<page>[a-zA-Z0-9_-]+)', [
@@ -73,7 +80,11 @@ class AppearanceController {
* Get all appearance settings
*/
public static function get_settings(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$stored = get_option(self::OPTION_KEY, []);
$defaults = self::get_default_settings();
// Merge stored with defaults to ensure all fields exist (recursive)
$settings = array_replace_recursive($defaults, $stored);
return new WP_REST_Response([
'success' => true,
@@ -85,8 +96,12 @@ class AppearanceController {
* Save general settings
*/
public static function save_general(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$settings = get_option(self::OPTION_KEY, []);
$defaults = self::get_default_settings();
$settings = array_replace_recursive($defaults, $settings);
$colors = $request->get_param('colors') ?? [];
$general_data = [
'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
'spa_page' => absint($request->get_param('spaPage') ?? 0),
@@ -101,11 +116,13 @@ class AppearanceController {
'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'),
'primary' => sanitize_hex_color($colors['primary'] ?? '#1a1a1a'),
'secondary' => sanitize_hex_color($colors['secondary'] ?? '#6b7280'),
'accent' => sanitize_hex_color($colors['accent'] ?? '#3b82f6'),
'text' => sanitize_hex_color($colors['text'] ?? '#111827'),
'background' => sanitize_hex_color($colors['background'] ?? '#ffffff'),
'gradientStart' => sanitize_hex_color($colors['gradientStart'] ?? '#9333ea'),
'gradientEnd' => sanitize_hex_color($colors['gradientEnd'] ?? '#3b82f6'),
],
];
@@ -230,6 +247,44 @@ class AppearanceController {
'data' => $footer_data,
], 200);
}
/**
* Save menu settings
*/
public static function save_menus(WP_REST_Request $request) {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$menus = $request->get_param('menus') ?? [];
// Sanitize menus
$sanitized_menus = [
'primary' => [],
'mobile' => [], // Optional separate mobile menu
];
foreach (['primary', 'mobile'] as $location) {
if (isset($menus[$location]) && is_array($menus[$location])) {
foreach ($menus[$location] as $item) {
$sanitized_menus[$location][] = [
'id' => sanitize_text_field($item['id'] ?? uniqid()),
'label' => sanitize_text_field($item['label'] ?? ''),
'type' => sanitize_text_field($item['type'] ?? 'page'), // page, custom
'value' => sanitize_text_field($item['value'] ?? ''), // slug or url
'target' => sanitize_text_field($item['target'] ?? '_self'),
];
}
}
}
$settings['menus'] = $sanitized_menus;
update_option(self::OPTION_KEY, $settings);
return new WP_REST_Response([
'success' => true,
'message' => 'Menu settings saved successfully',
'data' => $sanitized_menus,
], 200);
}
/**
* Save page-specific settings
@@ -389,11 +444,23 @@ class AppearanceController {
'sort_order' => 'ASC',
]);
$pages_list = array_map(function($page) {
$store_pages = [
(int) get_option('woocommerce_shop_page_id'),
(int) get_option('woocommerce_cart_page_id'),
(int) get_option('woocommerce_checkout_page_id'),
(int) get_option('woocommerce_myaccount_page_id'),
];
$pages_list = array_map(function($page) use ($store_pages) {
$is_woonoow = !empty(get_post_meta($page->ID, '_wn_page_structure', true));
$is_store = in_array((int)$page->ID, $store_pages, true);
return [
'id' => $page->ID,
'title' => $page->post_title,
'slug' => $page->post_name,
'is_woonoow_page' => $is_woonoow,
'is_store_page' => $is_store,
];
}, $pages);
@@ -427,6 +494,8 @@ class AppearanceController {
'accent' => '#3b82f6',
'text' => '#111827',
'background' => '#ffffff',
'gradientStart' => '#9333ea',
'gradientEnd' => '#3b82f6',
],
],
'header' => [
@@ -458,6 +527,14 @@ class AppearanceController {
],
'social_links' => [],
],
'menus' => [
'primary' => [
['id' => 'home', 'label' => 'Home', 'type' => 'page', 'value' => '/', 'target' => '_self'],
['id' => 'shop', 'label' => 'Shop', 'type' => 'page', 'value' => 'shop', 'target' => '_self'],
],
// Fallback for mobile if empty is to use primary
'mobile' => [],
],
'pages' => [
'shop' => [
'layout' => [

View File

@@ -8,6 +8,9 @@ class Menu {
add_action('admin_head', [__CLASS__, 'localize_wc_menus'], 999);
// Add link to standalone admin in admin bar
add_action('admin_bar_menu', [__CLASS__, 'add_admin_bar_link'], 100);
// Add custom state for SPA Front Page
add_filter('display_post_states', [__CLASS__, 'add_spa_page_state'], 10, 2);
}
public static function register() {
add_menu_page(
@@ -133,4 +136,23 @@ class Menu {
}
}
/**
* Add "WooNooW SPA Page" state to the pages list
*
* @param array $states Array of post states.
* @param \WP_Post $post Current post object.
* @return array Modified post states.
*/
public static function add_spa_page_state($states, $post) {
$settings = get_option('woonoow_appearance_settings', []);
$spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0;
if ((int)$post->ID === (int)$spa_frontpage_id) {
$states['spa_frontpage'] = __('WooNooW Front Page', 'woonoow');
} elseif (!empty(get_post_meta($post->ID, '_wn_page_structure', true))) {
$states['woonoow_page'] = __('WooNooW Page', 'woonoow');
}
return $states;
}
}

View File

@@ -226,6 +226,26 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
// POST /woonoow/v1/notifications/templates/:eventId/:channelId/send-test
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P<eventId>[a-zA-Z0-9_-]+)/(?P<channelId>[a-zA-Z0-9_-]+)/send-test', [
[
'methods' => 'POST',
'callback' => [$this, 'send_test_email'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'email' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
],
'recipient' => [
'default' => 'customer',
'type' => 'string',
],
],
],
]);
}
/**
@@ -931,4 +951,411 @@ class NotificationsController {
'per_page' => $per_page,
], 200);
}
/**
* Send test email for a notification template
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
public function send_test_email(WP_REST_Request $request) {
$event_id = $request->get_param('eventId');
$channel_id = $request->get_param('channelId');
$recipient_type = $request->get_param('recipient') ?? 'customer';
$to_email = $request->get_param('email');
// Validate email
if (!is_email($to_email)) {
return new \WP_Error(
'invalid_email',
__('Invalid email address', 'woonoow'),
['status' => 400]
);
}
// Only support email channel for test
if ($channel_id !== 'email') {
return new \WP_Error(
'unsupported_channel',
__('Test sending is only available for email channel', 'woonoow'),
['status' => 400]
);
}
// Get template
$template = TemplateProvider::get_template($event_id, $channel_id, $recipient_type);
if (!$template) {
return new \WP_Error(
'template_not_found',
__('Template not found', 'woonoow'),
['status' => 404]
);
}
// Build sample data for variables
$sample_data = $this->get_sample_data_for_event($event_id);
// Replace variables in subject and body
$subject = '[TEST] ' . $this->replace_variables($template['subject'] ?? '', $sample_data);
$body_markdown = $this->replace_variables($template['body'] ?? '', $sample_data);
// Render email using EmailRenderer
$email_renderer = \WooNooW\Core\Notifications\EmailRenderer::instance();
// We need to manually render since we're not triggering a real event
$html = $this->render_test_email($body_markdown, $subject, $sample_data);
// Set content type to HTML
$headers = ['Content-Type: text/html; charset=UTF-8'];
// Send email
$sent = wp_mail($to_email, $subject, $html, $headers);
if (!$sent) {
return new \WP_Error(
'send_failed',
__('Failed to send test email. Check your mail server configuration.', 'woonoow'),
['status' => 500]
);
}
return new WP_REST_Response([
'success' => true,
'message' => sprintf(__('Test email sent to %s', 'woonoow'), $to_email),
], 200);
}
/**
* Get sample data for an event type
*
* @param string $event_id
* @return array
*/
private function get_sample_data_for_event($event_id) {
$base_data = [
'site_name' => get_bloginfo('name'),
'store_name' => get_bloginfo('name'),
'store_url' => home_url(),
'shop_url' => get_permalink(wc_get_page_id('shop')),
'my_account_url' => get_permalink(wc_get_page_id('myaccount')),
'support_email' => get_option('admin_email'),
'current_year' => date('Y'),
'customer_name' => 'John Doe',
'customer_first_name' => 'John',
'customer_last_name' => 'Doe',
'customer_email' => 'john@example.com',
'customer_phone' => '+1 234 567 8900',
'login_url' => wp_login_url(),
];
// Order-related events
if (strpos($event_id, 'order') !== false) {
$base_data = array_merge($base_data, [
'order_number' => '12345',
'order_id' => '12345',
'order_date' => date('F j, Y'),
'order_total' => wc_price(129.99),
'order_subtotal' => wc_price(109.99),
'order_tax' => wc_price(10.00),
'order_shipping' => wc_price(10.00),
'order_discount' => wc_price(0),
'order_status' => 'Processing',
'order_url' => '#',
'payment_method' => 'Credit Card',
'payment_status' => 'Paid',
'payment_date' => date('F j, Y'),
'transaction_id' => 'TXN123456789',
'shipping_method' => 'Standard Shipping',
'estimated_delivery' => date('F j', strtotime('+3 days')) . '-' . date('j', strtotime('+5 days')),
'completion_date' => date('F j, Y'),
'billing_address' => '123 Main St, City, State 12345, Country',
'shipping_address' => '123 Main St, City, State 12345, Country',
'tracking_number' => 'TRACK123456',
'tracking_url' => '#',
'shipping_carrier' => 'Standard Carrier',
'payment_retry_url' => '#',
'review_url' => '#',
'order_items' => $this->get_sample_order_items_html(),
'order_items_table' => $this->get_sample_order_items_html(),
]);
}
// Customer account events
if (strpos($event_id, 'customer') !== false || strpos($event_id, 'account') !== false) {
$base_data = array_merge($base_data, [
'customer_username' => 'johndoe',
'user_temp_password' => 'SamplePass123',
'reset_link' => '#',
'reset_key' => 'abc123xyz',
'user_login' => 'johndoe',
'user_email' => 'john@example.com',
]);
}
return $base_data;
}
/**
* Get sample order items HTML
*
* @return string
*/
private function get_sample_order_items_html() {
return '<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #ddd;">Product</th>
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #ddd;">Qty</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">Price</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<strong>Sample Product</strong><br>
<span style="color: #666; font-size: 13px;">Size: M, Color: Blue</span>
</td>
<td style="padding: 12px; text-align: center; border-bottom: 1px solid #eee;">2</td>
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">' . wc_price(59.98) . '</td>
</tr>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<strong>Another Product</strong><br>
<span style="color: #666; font-size: 13px;">Option: Standard</span>
</td>
<td style="padding: 12px; text-align: center; border-bottom: 1px solid #eee;">1</td>
<td style="padding: 12px; text-align: right; border-bottom: 1px solid #eee;">' . wc_price(50.01) . '</td>
</tr>
</tbody>
</table>';
}
/**
* Replace variables in text
*
* @param string $text
* @param array $variables
* @return string
*/
private function replace_variables($text, $variables) {
foreach ($variables as $key => $value) {
$text = str_replace('{' . $key . '}', $value, $text);
}
return $text;
}
/**
* Render test email HTML
*
* @param string $body_markdown
* @param string $subject
* @param array $variables
* @return string
*/
private function render_test_email($body_markdown, $subject, $variables) {
// Parse cards
$content = $this->parse_cards_for_test($body_markdown);
// Get appearance settings for colors
$appearance = get_option('woonoow_appearance_settings', []);
$colors = $appearance['general']['colors'] ?? [];
$primary_color = $colors['primary'] ?? '#7f54b3';
$secondary_color = $colors['secondary'] ?? '#7f54b3';
$hero_gradient_start = $colors['gradientStart'] ?? '#667eea';
$hero_gradient_end = $colors['gradientEnd'] ?? '#764ba2';
// Get email settings for branding
$email_settings = get_option('woonoow_email_settings', []);
$logo_url = $email_settings['logo_url'] ?? '';
$header_text = $email_settings['header_text'] ?? $variables['store_name'];
$footer_text = $email_settings['footer_text'] ?? sprintf('© %s %s. All rights reserved.', date('Y'), $variables['store_name']);
$footer_text = str_replace('{current_year}', date('Y'), $footer_text);
// Build header
if (!empty($logo_url)) {
$header = sprintf(
'<a href="%s"><img src="%s" alt="%s" style="max-width: 200px; max-height: 60px;"></a>',
esc_url($variables['store_url']),
esc_url($logo_url),
esc_attr($variables['store_name'])
);
} else {
$header = sprintf(
'<a href="%s" style="font-size: 24px; font-weight: 700; color: #333; text-decoration: none;">%s</a>',
esc_url($variables['store_url']),
esc_html($header_text)
);
}
// Build full HTML
$html = '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>' . esc_html($subject) . '</title>
<style>
body { font-family: "Inter", Arial, sans-serif; background: #f8f8f8; margin: 0; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; }
.header { padding: 32px; text-align: center; background: #f8f8f8; }
.card-gutter { padding: 0 16px; }
.card { background: #ffffff; border-radius: 8px; margin-bottom: 24px; padding: 32px 40px; width: 100%; box-sizing: border-box; }
.card-hero { background: linear-gradient(135deg, ' . esc_attr($hero_gradient_start) . ' 0%, ' . esc_attr($hero_gradient_end) . ' 100%); color: #ffffff; }
.card-hero * { color: #ffffff !important; }
.card-success { background-color: #f0fdf4; }
.card-info { background-color: #f0f7ff; }
.card-warning { background-color: #fff8e1; }
.card-basic { background: none; padding: 0; }
h1, h2, h3 { margin-top: 0; color: #333; }
p { font-size: 16px; line-height: 1.6; color: #555; margin-bottom: 16px; }
.button { display: inline-block; background: ' . esc_attr($primary_color) . '; color: #ffffff !important; padding: 14px 28px; border-radius: 6px; text-decoration: none; font-weight: 600; }
.button-outline { display: inline-block; background: transparent; color: ' . esc_attr($secondary_color) . ' !important; padding: 12px 26px; border: 2px solid ' . esc_attr($secondary_color) . '; border-radius: 6px; text-decoration: none; font-weight: 600; }
.footer { padding: 32px; text-align: center; color: #888; font-size: 13px; }
</style>
</head>
<body>
<div class="container">
<div class="header">' . $header . '</div>
<div class="card-gutter">' . $content . '</div>
<div class="footer"><p>' . nl2br(esc_html($footer_text)) . '</p></div>
</div>
</body>
</html>';
return $html;
}
/**
* Parse cards for test email
*
* @param string $content
* @return string
*/
private function parse_cards_for_test($content) {
// Parse [card:type] syntax
$content = preg_replace_callback(
'/\[card:(\w+)\](.*?)\[\/card\]/s',
function($matches) {
$type = $matches[1];
$card_content = $this->markdown_to_html($matches[2]);
return '<div class="card card-' . esc_attr($type) . '">' . $card_content . '</div>';
},
$content
);
// Parse [card type="..."] syntax
$content = preg_replace_callback(
'/\[card([^\]]*)\](.*?)\[\/card\]/s',
function($matches) {
$attrs = $matches[1];
$card_content = $this->markdown_to_html($matches[2]);
$type = 'default';
if (preg_match('/type=["\']([^"\']+)["\']/', $attrs, $type_match)) {
$type = $type_match[1];
}
return '<div class="card card-' . esc_attr($type) . '">' . $card_content . '</div>';
},
$content
);
// Parse buttons - new [button:style](url)Text[/button] syntax
$content = preg_replace_callback(
'/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/',
function($matches) {
$style = $matches[1];
$url = $matches[2];
$text = trim($matches[3]);
$class = $style === 'outline' ? 'button-outline' : 'button';
return '<p style="text-align: center;"><a href="' . esc_url($url) . '" class="' . $class . '">' . esc_html($text) . '</a></p>';
},
$content
);
// Parse buttons - old [button url="..." style="..."]Text[/button] syntax
$content = preg_replace_callback(
'/\[button\s+url=["\']([^"\']+)["\'](?:\s+style=["\'](\w+)["\'])?\]([^\[]+)\[\/button\]/',
function($matches) {
$url = $matches[1];
$style = $matches[2] ?? 'solid';
$text = trim($matches[3]);
$class = $style === 'outline' ? 'button-outline' : 'button';
return '<p style="text-align: center;"><a href="' . esc_url($url) . '" class="' . $class . '">' . esc_html($text) . '</a></p>';
},
$content
);
// If no cards found, wrap in default card
if (strpos($content, '<div class="card') === false) {
$content = '<div class="card">' . $this->markdown_to_html($content) . '</div>';
}
return $content;
}
/**
* Basic markdown to HTML conversion
*
* @param string $text
* @return string
*/
private function markdown_to_html($text) {
// Parse buttons FIRST - new [button:style](url)Text[/button] syntax
$text = preg_replace_callback(
'/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/',
function($matches) {
$style = $matches[1];
$url = $matches[2];
$btn_text = trim($matches[3]);
$class = $style === 'outline' ? 'button-outline' : 'button';
return '<p style="text-align: center;"><a href="' . esc_url($url) . '" class="' . $class . '">' . esc_html($btn_text) . '</a></p>';
},
$text
);
// Parse buttons - old [button url="..."] syntax
$text = preg_replace_callback(
'/\[button\s+url=["\']([^"\']+)["\'](?:\s+style=[\'"](\\w+)[\'"])?\]([^\[]+)\[\/button\]/',
function($matches) {
$url = $matches[1];
$style = $matches[2] ?? 'solid';
$btn_text = trim($matches[3]);
$class = $style === 'outline' ? 'button-outline' : 'button';
return '<p style="text-align: center;"><a href="' . esc_url($url) . '" class="' . $class . '">' . esc_html($btn_text) . '</a></p>';
},
$text
);
// Headers
$text = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $text);
$text = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $text);
$text = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $text);
// Bold
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
// Italic
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
// Links (but not button syntax - already handled above)
$text = preg_replace('/\[(?!button)([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $text);
// List items
$text = preg_replace('/^- (.+)$/m', '<li>$1</li>', $text);
$text = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $text);
// Paragraphs - wrap lines that aren't already wrapped
$lines = explode("\n", $text);
$result = [];
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
if (!preg_match('/^<(h[1-6]|ul|li|div|p|table|tr|td|th)/', $line)) {
$line = '<p>' . $line . '</p>';
}
$result[] = $line;
}
return implode("\n", $result);
}
}

View File

@@ -5,7 +5,9 @@ use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WooNooW\Frontend\PlaceholderRenderer;
use WooNooW\Frontend\PageSSR;
use WooNooW\Templates\TemplateRegistry;
/**
* Pages Controller
@@ -19,6 +21,13 @@ class PagesController
public static function register_routes()
{
$namespace = 'woonoow/v1';
// Unset SPA Landing (Must be before generic slug route)
register_rest_route($namespace, '/pages/unset-spa-landing', [
'methods' => 'POST',
'callback' => [__CLASS__, 'unset_spa_landing'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// List all pages and templates
register_rest_route($namespace, '/pages', [
@@ -41,6 +50,13 @@ class PagesController
],
]);
// Get template presets (Must be before generic template cpt route)
register_rest_route($namespace, '/templates/presets', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_template_presets'],
'permission_callback' => '__return_true',
]);
// Get/Save CPT templates
register_rest_route($namespace, '/templates/(?P<cpt>[a-zA-Z0-9_-]+)', [
[
@@ -61,6 +77,8 @@ class PagesController
'callback' => [__CLASS__, 'get_content_with_template'],
'permission_callback' => '__return_true',
]);
// Create new page
register_rest_route($namespace, '/pages', [
@@ -82,6 +100,22 @@ class PagesController
'callback' => [__CLASS__, 'render_template_preview'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Set page as SPA Landing (shown at SPA root route)
register_rest_route($namespace, '/pages/(?P<id>\d+)/set-as-spa-landing', [
'methods' => 'POST',
'callback' => [__CLASS__, 'set_as_spa_landing'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
// Delete page
register_rest_route($namespace, '/pages/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_page'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
}
/**
@@ -91,14 +125,26 @@ class PagesController
{
return current_user_can('manage_woocommerce');
}
/**
* Get available template presets
*/
public static function get_template_presets()
{
return new WP_REST_Response(TemplateRegistry::get_templates(), 200);
}
/**
* Get all pages and templates
* Get all editable pages (and templates)
*/
public static function get_pages(WP_REST_Request $request)
public static function get_pages()
{
$result = [];
// Get SPA settings
$settings = get_option('woonoow_appearance_settings', []);
$spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0;
// Get structural pages (pages with WooNooW structure)
$pages = get_posts([
'post_type' => 'page',
@@ -119,6 +165,7 @@ class PagesController
'title' => $page->post_title,
'url' => get_permalink($page),
'icon' => 'page',
'is_spa_frontpage' => (int)$page->ID === (int)$spa_frontpage_id,
];
}
@@ -155,6 +202,10 @@ class PagesController
$structure = get_post_meta($page->ID, '_wn_page_structure', true);
// Get SPA settings
$settings = get_option('woonoow_appearance_settings', []);
$spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0;
// Get SEO data (Yoast/Rank Math)
$seo = self::get_seo_data($page->ID);
@@ -163,8 +214,8 @@ class PagesController
'type' => 'page',
'slug' => $page->post_name,
'title' => $page->post_title,
'url' => get_permalink($page),
'seo' => $seo,
'is_spa_frontpage' => (int)$page->ID === (int)$spa_frontpage_id,
'structure' => $structure ?: ['sections' => []],
], 200);
}
@@ -198,6 +249,9 @@ class PagesController
update_post_meta($page->ID, '_wn_page_structure', $save_data);
// Invalidate SSR cache
delete_transient("wn_ssr_page_{$page->ID}");
return new WP_REST_Response([
'success' => true,
'page' => [
@@ -327,6 +381,53 @@ class PagesController
], 200);
}
/**
* Set page as SPA Landing (the page shown at SPA root route)
* This does NOT affect WordPress page_on_front setting.
*/
public static function set_as_spa_landing(WP_REST_Request $request) {
$id = (int)$request->get_param('id');
// Verify the page exists
$page = get_post($id);
if (!$page || $page->post_type !== 'page') {
return new WP_Error('invalid_page', 'Page not found', ['status' => 404]);
}
// Update WooNooW SPA settings - set this page as the SPA frontpage
$settings = get_option('woonoow_appearance_settings', []);
if (!isset($settings['general'])) {
$settings['general'] = [];
}
$settings['general']['spa_frontpage'] = $id;
update_option('woonoow_appearance_settings', $settings);
return new WP_REST_Response([
'success' => true,
'id' => $id,
'message' => 'SPA Landing page set successfully'
], 200);
}
/**
* Unset SPA Landing (the page shown at SPA root route)
* After unsetting, SPA will redirect to /shop or /checkout based on mode
*/
public static function unset_spa_landing(WP_REST_Request $request) {
// Update WooNooW SPA settings - clear the SPA frontpage
$settings = get_option('woonoow_appearance_settings', []);
if (isset($settings['general'])) {
$settings['general']['spa_frontpage'] = 0;
}
update_option('woonoow_appearance_settings', $settings);
return new WP_REST_Response([
'success' => true,
'message' => 'SPA Landing page unset. Root will now redirect to shop/checkout.'
], 200);
}
/**
* Create new page
*/
@@ -365,6 +466,15 @@ class PagesController
'created_at' => current_time('mysql'),
];
// Apply template if provided
$template_id = $body['templateId'] ?? null;
if ($template_id) {
$template = TemplateRegistry::get_template($template_id);
if ($template) {
$structure['sections'] = $template['sections'];
}
}
update_post_meta($page_id, '_wn_page_structure', $structure);
return new WP_REST_Response([
@@ -378,6 +488,42 @@ class PagesController
], 201);
}
/**
* Delete page
*/
public static function delete_page(WP_REST_Request $request) {
$id = (int)$request->get_param('id');
$page = get_post($id);
if (!$page || $page->post_type !== 'page') {
return new WP_Error('not_found', 'Page not found', ['status' => 404]);
}
// Check if it's the SPA front page
$settings = get_option('woonoow_appearance_settings', []);
$spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0;
if ((int)$id === (int)$spa_frontpage_id) {
// Unset SPA frontpage if deleting it
if (isset($settings['general'])) {
$settings['general']['spa_frontpage'] = 0;
update_option('woonoow_appearance_settings', $settings);
}
}
$deleted = wp_delete_post($id, true); // Force delete
if (!$deleted) {
return new WP_Error('delete_failed', 'Failed to delete page', ['status' => 500]);
}
return new WP_REST_Response([
'success' => true,
'id' => $id,
'message' => 'Page deleted successfully'
], 200);
}
// ========================================
// Helper Methods
// ========================================

View File

@@ -13,7 +13,7 @@ if ( ! defined('ABSPATH') ) exit;
*/
class NavigationRegistry {
const NAV_OPTION = 'wnw_nav_tree';
const NAV_VERSION = '1.1.0'; // Added Pages (Page Editor)
const NAV_VERSION = '1.2.0'; // Added Menus (Menu Editor)
/**
* Initialize hooks
@@ -170,6 +170,7 @@ class NavigationRegistry {
'children' => [
['label' => __('General', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/general'],
['label' => __('Pages', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/pages'],
['label' => __('Menus', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/menus'],
['label' => __('Header', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/header'],
['label' => __('Footer', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/footer'],
['label' => __('Shop', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/shop'],

View File

@@ -373,13 +373,15 @@ class EmailRenderer {
$content = MarkdownParser::parse($content);
// Get email customization settings for colors
$email_settings = get_option('woonoow_email_settings', []);
$primary_color = $email_settings['primary_color'] ?? '#7f54b3';
$secondary_color = $email_settings['secondary_color'] ?? '#7f54b3';
$button_text_color = $email_settings['button_text_color'] ?? '#ffffff';
$hero_gradient_start = $email_settings['hero_gradient_start'] ?? '#667eea';
$hero_gradient_end = $email_settings['hero_gradient_end'] ?? '#764ba2';
$hero_text_color = $email_settings['hero_text_color'] ?? '#ffffff';
// Use unified colors from Appearance > General > Colors
$appearance = get_option('woonoow_appearance_settings', []);
$colors = $appearance['general']['colors'] ?? [];
$primary_color = $colors['primary'] ?? '#7f54b3';
$secondary_color = $colors['secondary'] ?? '#7f54b3';
$button_text_color = '#ffffff'; // Always white on primary buttons
$hero_gradient_start = $colors['gradientStart'] ?? '#667eea';
$hero_gradient_end = $colors['gradientEnd'] ?? '#764ba2';
$hero_text_color = '#ffffff'; // Always white on gradient
// Parse button shortcodes with FULL INLINE STYLES for Gmail compatibility
// Helper function to generate button HTML

View File

@@ -144,11 +144,11 @@ class Assets {
$theme_settings = array_replace_recursive($default_settings, $spa_settings);
// Get appearance settings and preload them
$appearance_settings = get_option('woonoow_appearance_settings', []);
if (empty($appearance_settings)) {
// Use defaults from AppearanceController
$appearance_settings = \WooNooW\Admin\AppearanceController::get_default_settings();
}
$stored_settings = get_option('woonoow_appearance_settings', []);
$default_appearance = \WooNooW\Admin\AppearanceController::get_default_settings();
// Merge stored settings with defaults to ensure new fields (like gradient colors) exist
$appearance_settings = array_replace_recursive($default_appearance, $stored_settings);
// Get WooCommerce currency settings
$currency_settings = [
@@ -198,12 +198,23 @@ class Assets {
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_page = $spa_page_id ? get_post($spa_page_id) : null;
// Check if SPA page is set as WordPress frontpage
// Check if SPA Entry 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;
$is_spa_wp_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');
// Get SPA Landing page (explicit setting, separate from Entry Page)
// This determines what content to show at the SPA root route "/"
$spa_frontpage_id = $appearance_settings['general']['spa_frontpage'] ?? 0;
$front_page_slug = '';
if ($spa_frontpage_id) {
$spa_frontpage = get_post($spa_frontpage_id);
if ($spa_frontpage) {
$front_page_slug = $spa_frontpage->post_name;
}
}
// If SPA Entry Page is WP frontpage, base path is /, otherwise use Entry Page slug
$base_path = $is_spa_wp_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;
@@ -223,6 +234,8 @@ class Assets {
'appearanceSettings' => $appearance_settings,
'basePath' => $base_path,
'useBrowserRouter' => $use_browser_router,
'frontPageSlug' => $front_page_slug,
'spaMode' => $appearance_settings['general']['spa_mode'] ?? 'full',
];
?>
@@ -270,11 +283,11 @@ class Assets {
return true;
}
// Get Customer SPA settings
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
// Get SPA mode from appearance settings (the correct source)
$appearance_settings = get_option('woonoow_appearance_settings', []);
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// If disabled, don't load
// If disabled, only load for pages with shortcodes
if ($mode === 'disabled') {
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
if (function_exists('is_shop') && is_shop()) {

View File

@@ -49,10 +49,13 @@ class PageSSR
// Generate section ID for anchor links
$section_id = $section['id'] ?? 'section-' . uniqid();
$element_styles = $section['elementStyles'] ?? [];
$styles = $section['styles'] ?? []; // Section wrapper styles (bg, overlay)
// Render based on section type
$method = 'render_' . str_replace('-', '_', $type);
if (method_exists(__CLASS__, $method)) {
return self::$method($resolved_props, $layout, $color_scheme, $section_id);
return self::$method($resolved_props, $layout, $color_scheme, $section_id, $element_styles, $styles);
}
// Fallback: generic section wrapper
@@ -95,10 +98,25 @@ class PageSSR
// Section Renderers
// ========================================
/**
* Helper to generate style attribute string
*/
private static function generate_style_attr($styles) {
if (empty($styles)) return '';
$css = [];
if (!empty($styles['color'])) $css[] = "color: {$styles['color']}";
if (!empty($styles['backgroundColor'])) $css[] = "background-color: {$styles['backgroundColor']}";
if (!empty($styles['fontSize'])) $css[] = "font-size: {$styles['fontSize']}"; // Note: assumes value has unit or is handled by class, but inline style works for specific values
// Add more mapping if needed, or rely on frontend to send valid CSS values
return empty($css) ? '' : 'style="' . implode(';', $css) . '"';
}
/**
* Render Hero section
*/
public static function render_hero($props, $layout, $color_scheme, $id)
public static function render_hero($props, $layout, $color_scheme, $id, $element_styles = [], $section_styles = [])
{
$title = esc_html($props['title'] ?? '');
$subtitle = esc_html($props['subtitle'] ?? '');
@@ -106,21 +124,50 @@ class PageSSR
$cta_text = esc_html($props['cta_text'] ?? '');
$cta_url = esc_url($props['cta_url'] ?? '');
$html = "<section id=\"{$id}\" class=\"wn-section wn-hero wn-hero--{$layout} wn-scheme--{$color_scheme}\">";
// Section Styles (Background & Spacing)
$bg_color = $section_styles['backgroundColor'] ?? '';
$bg_image = $section_styles['backgroundImage'] ?? '';
$overlay_opacity = $section_styles['backgroundOverlay'] ?? 0;
$pt = $section_styles['paddingTop'] ?? '';
$pb = $section_styles['paddingBottom'] ?? '';
$height_preset = $section_styles['heightPreset'] ?? '';
if ($image) {
$html .= "<img src=\"{$image}\" alt=\"{$title}\" class=\"wn-hero__image\" />";
$section_css = "";
if ($bg_color) $section_css .= "background-color: {$bg_color};";
if ($bg_image) $section_css .= "background-image: url('{$bg_image}'); background-size: cover; background-position: center;";
if ($pt) $section_css .= "padding-top: {$pt};";
if ($pb) $section_css .= "padding-bottom: {$pb};";
if ($height_preset === 'screen') $section_css .= "min-height: 100vh; display: flex; align-items: center;";
$section_attr = $section_css ? "style=\"{$section_css}\"" : "";
$html = "<section id=\"{$id}\" class=\"wn-section wn-hero wn-hero--{$layout} wn-scheme--{$color_scheme}\" {$section_attr}>";
// Overlay
if ($overlay_opacity > 0) {
$opacity = $overlay_opacity / 100;
$html .= "<div class=\"wn-hero__overlay\" style=\"background-color: rgba(0,0,0,{$opacity}); position: absolute; inset: 0;\"></div>";
}
// Element Styles
$title_style = self::generate_style_attr($element_styles['title'] ?? []);
$subtitle_style = self::generate_style_attr($element_styles['subtitle'] ?? []);
$cta_style = self::generate_style_attr($element_styles['cta_text'] ?? []); // Button
// Image (if not background)
if ($image && !$bg_image && $layout !== 'default') {
$html .= "<img src=\"{$image}\" alt=\"{$title}\" class=\"wn-hero__image\" />";
}
$html .= '<div class="wn-hero__content">';
$html .= '<div class="wn-hero__content" style="position: relative; z-index: 10;">';
if ($title) {
$html .= "<h1 class=\"wn-hero__title\">{$title}</h1>";
$html .= "<h1 class=\"wn-hero__title\" {$title_style}>{$title}</h1>";
}
if ($subtitle) {
$html .= "<p class=\"wn-hero__subtitle\">{$subtitle}</p>";
$html .= "<p class=\"wn-hero__subtitle\" {$subtitle_style}>{$subtitle}</p>";
}
if ($cta_text && $cta_url) {
$html .= "<a href=\"{$cta_url}\" class=\"wn-hero__cta\">{$cta_text}</a>";
$html .= "<a href=\"{$cta_url}\" class=\"wn-hero__cta\" {$cta_style}>{$cta_text}</a>";
}
$html .= '</div>';
$html .= '</section>';
@@ -128,50 +175,154 @@ class PageSSR
return $html;
}
/**
* Universal Row Renderer (Shared logic for Content & ImageText)
*/
private static function render_universal_row($props, $layout, $color_scheme, $element_styles, $options = []) {
$title = esc_html($props['title']['value'] ?? ($props['title'] ?? ''));
$text = $props['text']['value'] ?? ($props['text'] ?? ($props['content']['value'] ?? ($props['content'] ?? ''))); // Handle both props/values
$image = esc_url($props['image']['value'] ?? ($props['image'] ?? ''));
// Options
$has_image = !empty($image);
$image_pos = $layout ?: 'left';
// Element Styles
$title_style = self::generate_style_attr($element_styles['title'] ?? []);
$text_style = self::generate_style_attr($element_styles['text'] ?? ($element_styles['content'] ?? []));
// Wrapper Classes
$wrapper_class = "wn-max-w-7xl wn-mx-auto wn-px-4";
$grid_class = "wn-mx-auto";
if ($has_image && in_array($image_pos, ['left', 'right', 'image-left', 'image-right'])) {
$grid_class .= " wn-grid wn-grid-cols-1 wn-lg-grid-cols-2 wn-gap-12 wn-items-center";
} else {
$grid_class .= " wn-max-w-4xl";
}
$html = "<div class=\"{$wrapper_class}\">";
$html .= "<div class=\"{$grid_class}\">";
// Image Output
$image_html = "";
if ($current_pos_right = ($image_pos === 'right' || $image_pos === 'image-right')) {
$order_class = 'wn-lg-order-last';
} else {
$order_class = 'wn-lg-order-first';
}
if ($has_image) {
$image_html = "<div class=\"wn-relative wn-w-full wn-aspect-[4/3] wn-rounded-2xl wn-overflow-hidden wn-shadow-lg {$order_class}\">";
$image_html .= "<img src=\"{$image}\" alt=\"{$title}\" class=\"wn-absolute wn-inset-0 wn-w-full wn-h-full wn-object-cover\" />";
$image_html .= "</div>";
}
// Content Output
$content_html = "<div class=\"wn-flex wn-flex-col\">";
if ($title) {
$content_html .= "<h2 class=\"wn-text-3xl wn-font-bold wn-mb-6\" {$title_style}>{$title}</h2>";
}
if ($text) {
// Apply prose classes similar to React
$content_html .= "<div class=\"wn-prose wn-prose-lg wn-max-w-none\" {$text_style}>{$text}</div>";
}
$content_html .= "</div>";
// Render based on order (Grid handles order via CSS classes for left/right, but fallback for DOM order)
if ($has_image) {
// For grid layout, we output both. CSS order handles visual.
$html .= $image_html . $content_html;
} else {
$html .= $content_html;
}
$html .= "</div></div>";
return $html;
}
/**
* Render Content section (for post body, rich text)
*/
public static function render_content($props, $layout, $color_scheme, $id)
public static function render_content($props, $layout, $color_scheme, $id, $element_styles = [], $section_styles = [])
{
$content = $props['content'] ?? '';
// Apply WordPress content filters (shortcodes, autop, etc.)
$content = apply_filters('the_content', $content);
// Normalize prop structure for universal renderer if needed
if (is_string($props['content'])) {
$props['content'] = ['value' => $content];
} else {
$props['content']['value'] = $content;
}
// Section Styles (Background)
$bg_color = $section_styles['backgroundColor'] ?? '';
$padding = $section_styles['paddingTop'] ?? '';
$height_preset = $section_styles['heightPreset'] ?? '';
$css = "";
if($bg_color) $css .= "background-color:{$bg_color};";
return "<section id=\"{$id}\" class=\"wn-section wn-content wn-scheme--{$color_scheme}\">{$content}</section>";
// Height Logic
if ($height_preset === 'screen') {
$css .= "min-height: 100vh; display: flex; align-items: center;";
$padding = '5rem'; // Default padding for screen to avoid edge collision
} elseif ($height_preset === 'small') {
$padding = '2rem';
} elseif ($height_preset === 'large') {
$padding = '8rem';
} elseif ($height_preset === 'medium') {
$padding = '4rem';
}
if($padding) $css .= "padding:{$padding} 0;";
$style_attr = $css ? "style=\"{$css}\"" : "";
$inner_html = self::render_universal_row($props, 'left', $color_scheme, $element_styles);
return "<section id=\"{$id}\" class=\"wn-section wn-content wn-scheme--{$color_scheme}\" {$style_attr}>{$inner_html}</section>";
}
/**
* Render Image + Text section
*/
public static function render_image_text($props, $layout, $color_scheme, $id)
public static function render_image_text($props, $layout, $color_scheme, $id, $element_styles = [], $section_styles = [])
{
$title = esc_html($props['title'] ?? '');
$text = wp_kses_post($props['text'] ?? '');
$image = esc_url($props['image'] ?? '');
$html = "<section id=\"{$id}\" class=\"wn-section wn-image-text wn-image-text--{$layout} wn-scheme--{$color_scheme}\">";
if ($image) {
$html .= "<div class=\"wn-image-text__image\"><img src=\"{$image}\" alt=\"{$title}\" /></div>";
$bg_color = $section_styles['backgroundColor'] ?? '';
$padding = $section_styles['paddingTop'] ?? '';
$height_preset = $section_styles['heightPreset'] ?? '';
$css = "";
if($bg_color) $css .= "background-color:{$bg_color};";
// Height Logic
if ($height_preset === 'screen') {
$css .= "min-height: 100vh; display: flex; align-items: center;";
$padding = '5rem';
} elseif ($height_preset === 'small') {
$padding = '2rem';
} elseif ($height_preset === 'large') {
$padding = '8rem';
} elseif ($height_preset === 'medium') {
$padding = '4rem';
}
$html .= '<div class="wn-image-text__content">';
if ($title) {
$html .= "<h2 class=\"wn-image-text__title\">{$title}</h2>";
}
if ($text) {
$html .= "<div class=\"wn-image-text__text\">{$text}</div>";
}
$html .= '</div>';
$html .= '</section>';
return $html;
if($padding) $css .= "padding:{$padding} 0;";
$style_attr = $css ? "style=\"{$css}\"" : "";
$inner_html = self::render_universal_row($props, $layout, $color_scheme, $element_styles);
return "<section id=\"{$id}\" class=\"wn-section wn-image-text wn-scheme--{$color_scheme}\" {$style_attr}>{$inner_html}</section>";
}
/**
* Render Feature Grid section
*/
public static function render_feature_grid($props, $layout, $color_scheme, $id)
public static function render_feature_grid($props, $layout, $color_scheme, $id, $element_styles = [])
{
$heading = esc_html($props['heading'] ?? '');
$items = $props['items'] ?? [];
@@ -182,21 +333,36 @@ class PageSSR
$html .= "<h2 class=\"wn-feature-grid__heading\">{$heading}</h2>";
}
// Feature Item Styles (Card)
$item_style_attr = self::generate_style_attr($element_styles['feature_item'] ?? []); // BG, Border, Shadow handled by CSS classes mostly, but colors here
$item_bg = $element_styles['feature_item']['backgroundColor'] ?? '';
$html .= '<div class="wn-feature-grid__items">';
foreach ($items as $item) {
$item_title = esc_html($item['title'] ?? '');
$item_desc = esc_html($item['description'] ?? '');
$item_icon = esc_html($item['icon'] ?? '');
$html .= '<div class="wn-feature-grid__item">';
// Allow overriding item specific style if needed, but for now global
$html .= "<div class=\"wn-feature-grid__item\" {$item_style_attr}>";
// Render Icon SVG
if ($item_icon) {
$html .= "<span class=\"wn-feature-grid__icon\">{$item_icon}</span>";
$icon_svg = self::get_icon_svg($item_icon);
if ($icon_svg) {
$html .= "<div class=\"wn-feature-grid__icon\">{$icon_svg}</div>";
}
}
if ($item_title) {
$html .= "<h3 class=\"wn-feature-grid__item-title\">{$item_title}</h3>";
// Feature title style
$f_title_style = self::generate_style_attr($element_styles['feature_title'] ?? []);
$html .= "<h3 class=\"wn-feature-grid__item-title\" {$f_title_style}>{$item_title}</h3>";
}
if ($item_desc) {
$html .= "<p class=\"wn-feature-grid__item-desc\">{$item_desc}</p>";
// Feature description style
$f_desc_style = self::generate_style_attr($element_styles['feature_description'] ?? []);
$html .= "<p class=\"wn-feature-grid__item-desc\" {$f_desc_style}>{$item_desc}</p>";
}
$html .= '</div>';
}
@@ -209,7 +375,7 @@ class PageSSR
/**
* Render CTA Banner section
*/
public static function render_cta_banner($props, $layout, $color_scheme, $id)
public static function render_cta_banner($props, $layout, $color_scheme, $id, $element_styles = [])
{
$title = esc_html($props['title'] ?? '');
$text = esc_html($props['text'] ?? '');
@@ -238,13 +404,29 @@ class PageSSR
/**
* Render Contact Form section
*/
public static function render_contact_form($props, $layout, $color_scheme, $id)
public static function render_contact_form($props, $layout, $color_scheme, $id, $element_styles = [])
{
$title = esc_html($props['title'] ?? '');
$webhook_url = esc_url($props['webhook_url'] ?? '');
$redirect_url = esc_url($props['redirect_url'] ?? '');
$fields = $props['fields'] ?? ['name', 'email', 'message'];
// Extract styles
$btn_bg = $element_styles['button']['backgroundColor'] ?? '';
$btn_color = $element_styles['button']['color'] ?? '';
$field_bg = $element_styles['fields']['backgroundColor'] ?? '';
$field_color = $element_styles['fields']['color'] ?? '';
$btn_style = "";
if ($btn_bg) $btn_style .= "background-color: {$btn_bg};";
if ($btn_color) $btn_style .= "color: {$btn_color};";
$btn_attr = $btn_style ? "style=\"{$btn_style}\"" : "";
$field_style = "";
if ($field_bg) $field_style .= "background-color: {$field_bg};";
if ($field_color) $field_style .= "color: {$field_color};";
$field_attr = $field_style ? "style=\"{$field_style}\"" : "";
$html = "<section id=\"{$id}\" class=\"wn-section wn-contact-form wn-scheme--{$color_scheme}\">";
if ($title) {
@@ -259,19 +441,38 @@ class PageSSR
$html .= '<div class="wn-contact-form__field">';
$html .= "<label>{$field_label}</label>";
if ($field === 'message') {
$html .= "<textarea name=\"{$field}\" placeholder=\"{$field_label}\"></textarea>";
$html .= "<textarea name=\"{$field}\" placeholder=\"{$field_label}\" {$field_attr}></textarea>";
} else {
$html .= "<input type=\"text\" name=\"{$field}\" placeholder=\"{$field_label}\" />";
$html .= "<input type=\"text\" name=\"{$field}\" placeholder=\"{$field_label}\" {$field_attr} />";
}
$html .= '</div>';
}
$html .= '<button type="submit">Submit</button>';
$html .= "<button type=\"submit\" {$btn_attr}>Submit</button>";
$html .= '</form>';
$html .= '</section>';
return $html;
}
/**
* Helper to get SVG for known icons
*/
private static function get_icon_svg($name) {
$icons = [
'Star' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'Zap' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
'Shield' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
'Heart' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'Award' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg>',
'Clock' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
'Truck' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>',
'User' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
'Settings' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
];
return $icons[$name] ?? $icons['Star'];
}
/**
* Generic section fallback

View File

@@ -166,7 +166,14 @@ class TemplateOverride
'top'
);
} else {
// Rewrite /slug/anything to serve the SPA page
// Rewrite /slug to serve the SPA page (base URL)
add_rewrite_rule(
'^' . preg_quote($spa_slug, '/') . '/?$',
'index.php?page_id=' . $spa_page_id,
'top'
);
// Rewrite /slug/anything to serve the SPA page with path
// React Router handles the path after that
add_rewrite_rule(
'^' . preg_quote($spa_slug, '/') . '/(.*)$',
@@ -306,8 +313,30 @@ class TemplateOverride
wp_redirect($build_route('my-account'), 302);
exit;
}
// Redirect structural pages with WooNooW sections to SPA
if (is_singular('page') && $post) {
// Skip the SPA page itself and frontpage
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'])) {
// Redirect to SPA with page slug route
$page_slug = $post->post_name;
wp_redirect($build_route($page_slug), 302);
exit;
}
}
}
/**
* 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
@@ -331,8 +360,19 @@ class TemplateOverride
return; // SPA is not frontpage, let normal routing handle it
}
// Get the current request path
// Get the current request path relative to site root
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
$home_path = parse_url(home_url(), PHP_URL_PATH);
// Normalize request URI for subdirectory installs
if ($home_path && $home_path !== '/') {
$home_path = rtrim($home_path, '/');
if (strpos($request_uri, $home_path) === 0) {
$request_uri = substr($request_uri, strlen($home_path));
if (empty($request_uri)) $request_uri = '/';
}
}
$path = parse_url($request_uri, PHP_URL_PATH);
$path = '/' . trim($path, '/');
@@ -365,6 +405,27 @@ class TemplateOverride
}
}
// Check for structural pages with WooNooW sections
if (!$should_serve_spa && !empty($path) && $path !== '/') {
// Try to find a page by slug matching the path
$slug = trim($path, '/');
// Handle nested slugs (get the last part as the page slug)
if (strpos($slug, '/') !== false) {
$slug_parts = explode('/', $slug);
$slug = end($slug_parts);
}
$page = get_page_by_path($slug);
if ($page) {
// Check if this page has WooNooW structure
$structure = get_post_meta($page->ID, '_wn_page_structure', true);
if (!empty($structure) && !empty($structure['sections'])) {
$should_serve_spa = true;
}
}
}
// Not a SPA route
if (!$should_serve_spa) {
return;
@@ -396,8 +457,8 @@ class TemplateOverride
*/
public static function disable_canonical_redirect($redirect_url, $requested_url)
{
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
$settings = get_option('woonoow_appearance_settings', []);
$mode = isset($settings['general']['spa_mode']) ? $settings['general']['spa_mode'] : 'disabled';
// Only disable redirects in full SPA mode
if ($mode !== 'full') {
@@ -405,6 +466,7 @@ class TemplateOverride
}
// Check if this is a SPA route
// We include /product/ and standard endpoints
$spa_routes = ['/product/', '/cart', '/checkout', '/my-account'];
foreach ($spa_routes as $route) {
@@ -733,6 +795,20 @@ class TemplateOverride
*/
public static function serve_ssr_content($page_id, $type = 'page', $post_obj = null)
{
// Generate cache key
$cache_id = $post_obj ? $post_obj->ID : $page_id;
$cache_key = "wn_ssr_{$type}_{$cache_id}";
// Check cache TTL (default 1 hour, filterable)
$cache_ttl = apply_filters('woonoow_ssr_cache_ttl', HOUR_IN_SECONDS);
// Try to get cached content
$cached = get_transient($cache_key);
if ($cached !== false) {
echo $cached;
exit;
}
// Get page structure
if ($type === 'page') {
$structure = get_post_meta($page_id, '_wn_page_structure', true);
@@ -783,7 +859,8 @@ class TemplateOverride
wp_trim_words(wp_strip_all_tags($post_obj->post_content), 30);
}
// Output SSR HTML
// Output SSR HTML - start output buffering for caching
ob_start();
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
@@ -825,6 +902,14 @@ class TemplateOverride
</body>
</html>
<?php
// Get buffered output
$output = ob_get_clean();
// Cache the output for bots (uses cache TTL from filter)
set_transient($cache_key, $output, $cache_ttl);
// Output and exit
echo $output;
exit;
}

View File

@@ -0,0 +1,373 @@
<?php
namespace WooNooW\Setup;
/**
* Default Pages Setup
* Creates default pages with WooNooW structure on plugin activation
*/
class DefaultPages
{
/**
* Ensure all default pages exist
*/
public static function create_pages()
{
self::create_home_page();
self::create_about_page();
self::create_contact_page();
self::create_legal_pages();
self::create_woocommerce_pages();
self::ensure_spa_settings();
}
/**
* Ensure SPA Settings are configured
*/
private static function ensure_spa_settings()
{
$settings = get_option('woonoow_appearance_settings', []);
// Ensure General array exists
if (!isset($settings['general'])) {
$settings['general'] = [];
}
// Enable SPA mode if not set
if (empty($settings['general']['spa_mode']) || $settings['general']['spa_mode'] === 'disabled') {
$settings['general']['spa_mode'] = 'full';
}
// Set SPA Root Page if missing (prioritize Home, then Shop)
if (empty($settings['general']['spa_page'])) {
$home_page = get_page_by_path('home');
$shop_page_id = get_option('woocommerce_shop_page_id');
if ($home_page) {
// If Home exists, make it the Front Page AND SPA Root
$settings['general']['spa_page'] = $home_page->ID;
update_option('show_on_front', 'page');
update_option('page_on_front', $home_page->ID);
} elseif ($shop_page_id) {
// Fallback to Shop
$settings['general']['spa_page'] = $shop_page_id;
}
}
update_option('woonoow_appearance_settings', $settings);
}
/**
* Create Home Page with Rich Layout
*/
private static function create_home_page()
{
$slug = 'home';
$title = 'Home';
if (self::page_exists($slug)) {
return;
}
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-hero-home',
'type' => 'hero',
'layoutVariant' => 'default',
'colorScheme' => 'primary',
'props' => [
'title' => ['type' => 'static', 'value' => 'Welcome onto WooNooW'],
'subtitle' => ['type' => 'static', 'value' => 'Discover our premium collection of products tailored just for you. Quality meets style in every item.'],
'cta_text' => ['type' => 'static', 'value' => 'Shop Now'],
'cta_url' => ['type' => 'static', 'value' => '/shop'],
'image' => ['type' => 'static', 'value' => 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=1600&q=80'],
],
'elementStyles' => [
'title' => ['fontSize' => 'text-5xl', 'fontWeight' => 'font-bold'],
]
],
[
'id' => 'section-features-home',
'type' => 'feature-grid',
'layoutVariant' => 'grid-3',
'props' => [
'heading' => ['type' => 'static', 'value' => 'Why Choose Us'],
'features' => ['type' => 'static', 'value' => json_encode([
['icon' => 'truck', 'title' => 'Free Shipping', 'description' => 'On all orders over $50'],
['icon' => 'shield', 'title' => 'Secure Payment', 'description' => '100% secure payment processing'],
['icon' => 'clock', 'title' => '24/7 Support', 'description' => 'Dedicated support anytime you need'],
])]
]
],
[
'id' => 'section-story-home',
'type' => 'image-text',
'layoutVariant' => 'image-left',
'props' => [
'title' => ['type' => 'static', 'value' => 'Our Story'],
'text' => ['type' => 'static', 'value' => 'Founded with a passion for quality and design, we strive to bring you products that elevate your everyday life. Every item is carefully curated and inspected to ensure it meets our high standards.'],
'image' => ['type' => 'static', 'value' => 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80'],
]
],
[
'id' => 'section-cta-home',
'type' => 'cta-banner',
'colorScheme' => 'muted',
'props' => [
'title' => ['type' => 'static', 'value' => 'Ready to start shopping?'],
'text' => ['type' => 'static', 'value' => 'Join thousands of satisfied customers today.'],
'button_text' => ['type' => 'static', 'value' => 'View Catalog'],
'button_url' => ['type' => 'static', 'value' => '/shop'],
]
]
],
'created_at' => current_time('mysql'),
];
self::insert_page($title, $slug, $structure);
}
/**
* Create About Us Page
*/
private static function create_about_page()
{
$slug = 'about';
$title = 'About Us';
if (self::page_exists($slug)) {
return;
}
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-hero-about',
'type' => 'hero',
'layoutVariant' => 'centered',
'colorScheme' => 'secondary',
'props' => [
'title' => ['type' => 'static', 'value' => 'About Us'],
'subtitle' => ['type' => 'static', 'value' => 'Learn more about our journey and mission.'],
'image' => ['type' => 'static', 'value' => 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1600&q=80'],
]
],
[
'id' => 'section-story-about',
'type' => 'image-text',
'layoutVariant' => 'image-right',
'props' => [
'title' => ['type' => 'static', 'value' => 'Who We Are'],
'text' => ['type' => 'static', 'value' => 'We are a team of passionate individuals dedicated to providing the best shopping experience. Our mission is to make quality products accessible to everyone.'],
'image' => ['type' => 'static', 'value' => 'https://images.unsplash.com/photo-1556761175-5973dc0f32e7?w=800&q=80'],
]
],
[
'id' => 'section-values-about',
'type' => 'feature-grid',
'layoutVariant' => 'grid-3',
'props' => [
'heading' => ['type' => 'static', 'value' => 'Our Core Values'],
'features' => ['type' => 'static', 'value' => json_encode([
['icon' => 'heart', 'title' => 'Passion', 'description' => 'We love what we do'],
['icon' => 'star', 'title' => 'Excellence', 'description' => 'We aim for the best'],
['icon' => 'users', 'title' => 'Community', 'description' => 'We build relationships'],
])]
]
]
],
'created_at' => current_time('mysql'),
];
self::insert_page($title, $slug, $structure);
}
/**
* Create Contact Page
*/
private static function create_contact_page()
{
$slug = 'contact';
$title = 'Contact Us';
if (self::page_exists($slug)) {
return;
}
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-hero-contact',
'type' => 'hero',
'layoutVariant' => 'default',
'colorScheme' => 'gradient',
'props' => [
'title' => ['type' => 'static', 'value' => 'Get in Touch'],
'subtitle' => ['type' => 'static', 'value' => 'Have questions? We are here to help.'],
]
],
[
'id' => 'section-form-contact',
'type' => 'contact-form',
'props' => [
'title' => ['type' => 'static', 'value' => 'Send us a Message'],
]
]
],
'created_at' => current_time('mysql'),
];
self::insert_page($title, $slug, $structure);
}
/**
* Create Legal Pages
*/
private static function create_legal_pages()
{
$pages = [
'privacy-policy' => 'Privacy Policy',
'terms-conditions' => 'Terms & Conditions',
];
foreach ($pages as $slug => $title) {
if (self::page_exists($slug)) {
continue;
}
$content = "<h2>{$title}</h2><p>This is a placeholder for your {$title}. Please update this content with your actual legal text.</p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>";
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-content-' . $slug,
'type' => 'content',
'layoutVariant' => 'narrow',
'props' => [
'content' => ['type' => 'static', 'value' => $content],
]
]
],
'created_at' => current_time('mysql'),
];
self::insert_page($title, $slug, $structure);
// If privacy policy, link it in WP settings
if ($slug === 'privacy-policy') {
$page = get_page_by_path($slug);
if ($page) {
update_option('wp_page_for_privacy_policy', $page->ID);
}
}
}
}
/**
* Create WooCommerce Pages (Shop, Cart, Checkout, My Account)
*/
private static function create_woocommerce_pages()
{
if (!class_exists('WooCommerce')) {
return;
}
$wc_pages = [
'shop' => ['title' => 'Shop', 'content' => '', 'option' => 'woocommerce_shop_page_id'],
'cart' => ['title' => 'Cart', 'content' => '<!-- wp:shortcode -->[woocommerce_cart]<!-- /wp:shortcode -->', 'option' => 'woocommerce_cart_page_id'],
'checkout' => ['title' => 'Checkout', 'content' => '<!-- wp:shortcode -->[woocommerce_checkout]<!-- /wp:shortcode -->', 'option' => 'woocommerce_checkout_page_id'],
'my-account' => ['title' => 'My Account', 'content' => '<!-- wp:shortcode -->[woocommerce_my_account]<!-- /wp:shortcode -->', 'option' => 'woocommerce_myaccount_page_id'],
];
foreach ($wc_pages as $slug => $data) {
// Check if page is already assigned in WC options
$existing_id = get_option($data['option']);
if ($existing_id && get_post($existing_id)) {
continue;
}
// Check if page exists by slug
if (self::page_exists($slug)) {
$page = get_page_by_path($slug);
update_option($data['option'], $page->ID);
continue;
}
// Create page
$page_id = wp_insert_post([
'post_title' => $data['title'],
'post_name' => $slug,
'post_content' => $data['content'],
'post_status' => 'publish',
'post_type' => 'page',
]);
if ($page_id && !is_wp_error($page_id)) {
update_option($data['option'], $page_id);
// For Shop page, add a fallback structure (though content-product takes precedence in SPA)
if ($slug === 'shop') {
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-shop-products',
'type' => 'content',
'props' => [
'content' => ['type' => 'static', 'value' => '[products limit="12" columns="4"]'],
]
]
],
'created_at' => current_time('mysql'),
];
update_post_meta($page_id, '_wn_page_structure', $structure);
} else {
// For other pages, add structre that wraps the shortcode for SPA rendering
$structure = [
'type' => 'page',
'sections' => [
[
'id' => 'section-' . $slug,
'type' => 'content',
'props' => [
'content' => ['type' => 'static', 'value' => $data['content']],
]
]
],
'created_at' => current_time('mysql'),
];
update_post_meta($page_id, '_wn_page_structure', $structure);
}
}
}
}
/**
* Helper: Check if page exists
*/
private static function page_exists($slug)
{
return !empty(get_page_by_path($slug));
}
/**
* Helper: Insert Page and Structure
*/
private static function insert_page($title, $slug, $structure)
{
$page_id = wp_insert_post([
'post_title' => $title,
'post_name' => $slug,
'post_status' => 'publish',
'post_type' => 'page',
]);
if ($page_id && !is_wp_error($page_id)) {
update_post_meta($page_id, '_wn_page_structure', $structure);
}
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace WooNooW\Templates;
defined('ABSPATH') || exit;
class TemplateRegistry
{
/**
* Get all available templates
*/
public static function get_templates()
{
return [
[
'id' => 'blank',
'label' => 'Blank Page',
'description' => 'Start from scratch with an empty page.',
'icon' => 'file',
'sections' => []
],
[
'id' => 'landing-page',
'label' => 'Landing Page',
'description' => 'High-converting landing page with Hero, Features, and CTA.',
'icon' => 'layout',
'sections' => self::get_landing_page_structure()
],
[
'id' => 'about-us',
'label' => 'About Us',
'description' => 'Tell your story with image-text layouts and content.',
'icon' => 'users',
'sections' => self::get_about_us_structure()
],
[
'id' => 'contact',
'label' => 'Contact',
'description' => 'Simple contact page with a form and address details.',
'icon' => 'mail',
'sections' => self::get_contact_structure()
]
];
}
/**
* Get a specific template by ID
*/
public static function get_template($id)
{
$templates = self::get_templates();
foreach ($templates as $template) {
if ($template['id'] === $id) {
return $template;
}
}
return null;
}
/**
* Helper to generate a unique ID
*/
private static function generate_id()
{
return uniqid('section_');
}
private static function get_landing_page_structure()
{
return [
[
'id' => self::generate_id(),
'type' => 'hero',
'props' => [
'title' => ['type' => 'static', 'value' => 'Welcome to Our Service'],
'subtitle' => ['type' => 'static', 'value' => 'We create amazing digital experiences for your business.'],
'cta_text' => ['type' => 'static', 'value' => 'Get Started'],
'cta_url' => ['type' => 'static', 'value' => '#'],
'image' => ['type' => 'static', 'value' => ''],
],
'styles' => ['contentWidth' => 'full']
],
[
'id' => self::generate_id(),
'type' => 'feature-grid',
'props' => [
'heading' => ['type' => 'static', 'value' => 'Why Choose Us'],
'features' => ['type' => 'static', 'value' => [
['title' => 'Fast Delivery', 'description' => 'Quick shipping to your doorstep'],
['title' => 'Secure Payment', 'description' => 'Your data is always protected'],
['title' => 'Quality Products', 'description' => 'Only the best for our customers']
]]
],
'styles' => ['contentWidth' => 'contained']
],
[
'id' => self::generate_id(),
'type' => 'cta-banner',
'props' => [
'title' => ['type' => 'static', 'value' => 'Ready to Launch?'],
'text' => ['type' => 'static', 'value' => 'Join thousands of satisfied customers today.'],
'button_text' => ['type' => 'static', 'value' => 'Sign Up Now'],
'button_url' => ['type' => 'static', 'value' => '#']
],
'styles' => ['contentWidth' => 'full']
]
];
}
private static function get_about_us_structure()
{
return [
[
'id' => self::generate_id(),
'type' => 'image-text',
'layoutVariant' => 'image-left',
'props' => [
'title' => ['type' => 'static', 'value' => 'Our Story'],
'text' => ['type' => 'static', 'value' => 'We started with a simple mission: to make web design accessible to everyone. Our journey began in a small garage...'],
'image' => ['type' => 'static', 'value' => '']
],
'styles' => ['contentWidth' => 'contained']
],
[
'id' => self::generate_id(),
'type' => 'image-text',
'layoutVariant' => 'image-right',
'props' => [
'title' => ['type' => 'static', 'value' => 'Our Vision'],
'text' => ['type' => 'static', 'value' => 'To empower businesses of all sizes to have a professional online presence without the technical headache.'],
'image' => ['type' => 'static', 'value' => '']
],
'styles' => ['contentWidth' => 'contained']
],
[
'id' => self::generate_id(),
'type' => 'content',
'props' => [
'content' => ['type' => 'static', 'value' => '<h3>Meet the Team</h3><p>Our diverse team of designers and developers...</p>']
],
'styles' => ['contentWidth' => 'contained']
]
];
}
private static function get_contact_structure()
{
return [
[
'id' => self::generate_id(),
'type' => 'content',
'props' => [
'content' => ['type' => 'static', 'value' => '<h2>Get in Touch</h2><p>We are here to help and answer any question you might have.</p><p><strong>Address:</strong><br>123 Web Street<br>Tech City, TC 90210</p>']
],
'styles' => ['contentWidth' => 'contained']
],
[
'id' => self::generate_id(),
'type' => 'contact-form',
'props' => [
'title' => ['type' => 'static', 'value' => 'Send us a Message'],
'webhook_url' => ['type' => 'static', 'value' => ''],
'redirect_url' => ['type' => 'static', 'value' => '']
],
'styles' => ['contentWidth' => 'contained']
]
];
}
}