Files
WooNooW/includes/Core/Notifications/EmailRenderer.php
dwindown 1573bff7b3 feat: Card-based email system implementation
##  Core Card System Complete!

### base.html Template
-  Single, theme-agnostic template
-  Card system CSS (default, highlight, info, warning, success, bg)
-  Customizable header (logo/text)
-  Customizable footer + social icons
-  Customizable body background
-  Mobile responsive
-  Email client compatible (Outlook, Gmail, etc.)

### EmailRenderer.php - Card Parser
-  `parse_cards()` - Parses [card]...[/card] syntax
-  `parse_card_attributes()` - Extracts type and bg attributes
-  `render_card()` - Renders card HTML
-  `render_card_spacing()` - 24px spacing between cards
-  `render_html()` - Email customization support
-  `get_social_icon_url()` - Social media icons

### Card Types Supported
```
[card]                        → Default white card
[card type="highlight"]       → Purple gradient card
[card type="info"]            → Blue info card
[card type="warning"]         → Yellow warning card
[card type="success"]         → Green success card
[card bg="https://..."]       → Background image card
```

### Email Customization
-  Header: Logo or text
-  Body background color
-  Footer text
-  Social media links (Facebook, Instagram, Twitter, LinkedIn)
-  Stored in `woonoow_notification_settings[email_appearance]`

### Default Templates Updated
-  order_placed_email - Multi-card layout
-  order_processing_email - Success card + summary
-  Other templates ready to update

---

**Architecture:**
```
Content with [card] tags
    ↓
parse_cards()
    ↓
render_card() × N
    ↓
base.html template
    ↓
Beautiful HTML email! 🎨
```

**Next:** Settings UI + Live Preview 🚀
2025-11-12 23:14:00 +07:00

459 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Email Renderer
*
* Renders email templates with content
*
* @package WooNooW\Core\Notifications
*/
namespace WooNooW\Core\Notifications;
class EmailRenderer {
/**
* Instance
*/
private static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Render email
*
* @param string $event_id Event ID (order_placed, order_processing, etc.)
* @param string $recipient_type Recipient type (staff, customer)
* @param mixed $data Order, Product, or Customer object
* @param array $extra_data Additional data
* @return array|null ['to', 'subject', 'body']
*/
public function render($event_id, $recipient_type, $data, $extra_data = []) {
// Get template settings
$template_settings = $this->get_template_settings($event_id, $recipient_type);
if (!$template_settings) {
return null;
}
// Get recipient email
$to = $this->get_recipient_email($recipient_type, $data);
if (!$to) {
return null;
}
// Get variables
$variables = $this->get_variables($event_id, $data, $extra_data);
// Replace variables in subject and content
$subject = $this->replace_variables($template_settings['subject'], $variables);
$content = $this->replace_variables($template_settings['body'], $variables);
// Parse cards in content
$content = $this->parse_cards($content);
// Get HTML template
$template_path = $this->get_design_template();
// Render final HTML
$html = $this->render_html($template_path, $content, $subject, $variables);
return [
'to' => $to,
'subject' => $subject,
'body' => $html,
];
}
/**
* Get template settings
*
* @param string $event_id
* @param string $recipient_type
* @return array|null
*/
private function get_template_settings($event_id, $recipient_type) {
// Get saved template
$template = TemplateProvider::get_template($event_id, 'email');
if (!$template) {
return null;
}
// Get design template preference
$settings = get_option('woonoow_notification_settings', []);
$design = $settings['email_design_template'] ?? 'modern';
return [
'subject' => $template['subject'] ?? '',
'body' => $template['body'] ?? '',
'design' => $design,
];
}
/**
* Get recipient email
*
* @param string $recipient_type
* @param mixed $data
* @return string|null
*/
private function get_recipient_email($recipient_type, $data) {
if ($recipient_type === 'staff') {
return get_option('admin_email');
}
// Customer
if ($data instanceof \WC_Order) {
return $data->get_billing_email();
}
if ($data instanceof \WC_Customer) {
return $data->get_email();
}
return null;
}
/**
* Get variables for template
*
* @param string $event_id
* @param mixed $data
* @param array $extra_data
* @return array
*/
private function get_variables($event_id, $data, $extra_data = []) {
$variables = [
'store_name' => get_bloginfo('name'),
'store_url' => home_url(),
'site_title' => get_bloginfo('name'),
];
// Order variables
if ($data instanceof \WC_Order) {
$variables = array_merge($variables, [
'order_number' => $data->get_order_number(),
'order_id' => $data->get_id(),
'order_date' => $data->get_date_created()->date('F j, Y'),
'order_total' => $data->get_formatted_order_total(),
'order_subtotal' => wc_price($data->get_subtotal()),
'order_tax' => wc_price($data->get_total_tax()),
'order_shipping' => wc_price($data->get_shipping_total()),
'order_discount' => wc_price($data->get_discount_total()),
'order_status' => wc_get_order_status_name($data->get_status()),
'order_url' => $data->get_view_order_url(),
'payment_method' => $data->get_payment_method_title(),
'shipping_method' => $data->get_shipping_method(),
'customer_name' => $data->get_formatted_billing_full_name(),
'customer_first_name' => $data->get_billing_first_name(),
'customer_last_name' => $data->get_billing_last_name(),
'customer_email' => $data->get_billing_email(),
'customer_phone' => $data->get_billing_phone(),
'billing_address' => $data->get_formatted_billing_address(),
'shipping_address' => $data->get_formatted_shipping_address(),
]);
// Order items
$items_html = '';
foreach ($data->get_items() as $item) {
$product = $item->get_product();
$items_html .= sprintf(
'<tr><td>%s × %d</td><td>%s</td></tr>',
$item->get_name(),
$item->get_quantity(),
wc_price($item->get_total())
);
}
$variables['order_items'] = $items_html;
}
// Product variables
if ($data instanceof \WC_Product) {
$variables = array_merge($variables, [
'product_id' => $data->get_id(),
'product_name' => $data->get_name(),
'product_sku' => $data->get_sku(),
'product_price' => wc_price($data->get_price()),
'product_url' => get_permalink($data->get_id()),
'stock_quantity' => $data->get_stock_quantity(),
'stock_status' => $data->get_stock_status(),
]);
}
// Customer variables
if ($data instanceof \WC_Customer) {
$variables = array_merge($variables, [
'customer_id' => $data->get_id(),
'customer_name' => $data->get_display_name(),
'customer_first_name' => $data->get_first_name(),
'customer_last_name' => $data->get_last_name(),
'customer_email' => $data->get_email(),
'customer_username' => $data->get_username(),
]);
}
// Merge extra data
$variables = array_merge($variables, $extra_data);
return apply_filters('woonoow_email_variables', $variables, $event_id, $data);
}
/**
* Parse [card] tags and convert to HTML
*
* @param string $content
* @return string
*/
private function parse_cards($content) {
// Match [card ...] ... [/card] patterns
preg_match_all('/\[card([^\]]*)\](.*?)\[\/card\]/s', $content, $matches, PREG_SET_ORDER);
if (empty($matches)) {
// No cards found, wrap entire content in a single card
return $this->render_card($content, []);
}
$html = '';
foreach ($matches as $match) {
$attributes = $this->parse_card_attributes($match[1]);
$card_content = $match[2];
$html .= $this->render_card($card_content, $attributes);
$html .= $this->render_card_spacing();
}
// Remove last spacing
$html = preg_replace('/<table[^>]*class="card-spacing"[^>]*>.*?<\/table>\s*$/s', '', $html);
return $html;
}
/**
* Parse card attributes from [card ...] tag
*
* @param string $attr_string
* @return array
*/
private function parse_card_attributes($attr_string) {
$attributes = [
'type' => 'default',
'bg' => null,
];
// Parse type="highlight"
if (preg_match('/type=["\']([^"\']+)["\']/', $attr_string, $match)) {
$attributes['type'] = $match[1];
}
// Parse bg="url"
if (preg_match('/bg=["\']([^"\']+)["\']/', $attr_string, $match)) {
$attributes['bg'] = $match[1];
}
return $attributes;
}
/**
* Render a single card
*
* @param string $content
* @param array $attributes
* @return string
*/
private function render_card($content, $attributes) {
$type = $attributes['type'] ?? 'default';
$bg = $attributes['bg'] ?? null;
$class = 'card';
$style = 'width: 100%; background-color: #ffffff; border-radius: 8px;';
// Add type class
if ($type !== 'default') {
$class .= ' card-' . esc_attr($type);
}
// Add background image
if ($bg) {
$class .= ' card-bg';
$style .= ' background-image: url(' . esc_url($bg) . ');';
}
return sprintf(
'<table role="presentation" class="%s" border="0" cellpadding="0" cellspacing="0" style="%s">
<tr>
<td class="content" style="padding: 32px 40px;">
%s
</td>
</tr>
</table>',
$class,
$style,
$content
);
}
/**
* Render card spacing
*
* @return string
*/
private function render_card_spacing() {
return '<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr><td class="card-spacing" style="height: 24px; font-size: 24px; line-height: 24px;">&nbsp;</td></tr>
</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;
}
/**
* Get design template path
*
* @return string
*/
private function get_design_template() {
// Use single base template (theme-agnostic)
$template_path = WOONOOW_PATH . 'templates/emails/base.html';
// Allow filtering template path
$template_path = apply_filters('woonoow_email_template', $template_path);
// Fallback to base if custom template doesn't exist
if (!file_exists($template_path)) {
$template_path = WOONOOW_PATH . 'templates/emails/base.html';
}
return $template_path;
}
/**
* Render HTML email
*
* @param string $template_path Path to HTML template
* @param string $content Email content (HTML)
* @param string $subject Email subject
* @param array $variables All variables
* @return string
*/
private function render_html($template_path, $content, $subject, $variables) {
if (!file_exists($template_path)) {
// Fallback to plain HTML
return $content;
}
// Load template
$html = file_get_contents($template_path);
// Get email customization settings
$settings = get_option('woonoow_notification_settings', []);
$email_settings = $settings['email_appearance'] ?? [];
// Email body background
$body_bg = $email_settings['body_bg'] ?? '#f8f8f8';
// Email header (logo or text)
$header_type = $email_settings['header_type'] ?? 'text';
if ($header_type === 'logo' && !empty($email_settings['header_logo'])) {
$header = sprintf(
'<a href="%s"><img src="%s" alt="%s" style="max-width: 140px;"></a>',
esc_url($variables['store_url']),
esc_url($email_settings['header_logo']),
esc_attr($variables['store_name'])
);
} else {
$header_text = $email_settings['header_text'] ?? $variables['store_name'];
$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)
);
}
// Email footer
$footer_text = $email_settings['footer_text'] ?? sprintf(
'© %s %s. All rights reserved.',
date('Y'),
$variables['store_name']
);
// Social icons
$social_html = '';
if (!empty($email_settings['social_links'])) {
$social_html = '<div class="social-icons" style="margin-top: 16px;">';
foreach ($email_settings['social_links'] as $platform => $url) {
if (!empty($url)) {
$social_html .= sprintf(
'<a href="%s" style="display: inline-block; margin: 0 8px;"><img src="%s" alt="%s" style="width: 24px; height: 24px;"></a>',
esc_url($url),
$this->get_social_icon_url($platform),
esc_attr(ucfirst($platform))
);
}
}
$social_html .= '</div>';
}
$footer = sprintf(
'<p style="font-family: \'Inter\', Arial, sans-serif; font-size: 13px; line-height: 1.5; color: #888888; margin: 0 0 8px 0;">%s</p>%s',
nl2br(esc_html($footer_text)),
$social_html
);
// Replace placeholders
$html = str_replace('{{email_subject}}', esc_html($subject), $html);
$html = str_replace('{{email_body_bg}}', esc_attr($body_bg), $html);
$html = str_replace('{{email_header}}', $header, $html);
$html = str_replace('{{email_content}}', $content, $html);
$html = str_replace('{{email_footer}}', $footer, $html);
$html = str_replace('{{store_name}}', esc_html($variables['store_name']), $html);
$html = str_replace('{{store_url}}', esc_url($variables['store_url']), $html);
$html = str_replace('{{current_year}}', date('Y'), $html);
// Replace all other variables
foreach ($variables as $key => $value) {
$html = str_replace('{{' . $key . '}}', $value, $html);
}
return $html;
}
/**
* Get social icon URL
*
* @param string $platform
* @return string
*/
private function get_social_icon_url($platform) {
// You can host these icons or use a CDN
$icons = [
'facebook' => 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
'instagram' => 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
'twitter' => 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
'linkedin' => 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
];
return $icons[$platform] ?? '';
}
}