Fix button roundtrip in editor, alignment persistence, and test email rendering
This commit is contained in:
@@ -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' => [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ========================================
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
373
includes/Setup/DefaultPages.php
Normal file
373
includes/Setup/DefaultPages.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
169
includes/Templates/TemplateRegistry.php
Normal file
169
includes/Templates/TemplateRegistry.php
Normal 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']
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user