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 🚀
This commit is contained in:
dwindown
2025-11-12 23:14:00 +07:00
parent 37f73da71d
commit 1573bff7b3
3 changed files with 599 additions and 14 deletions

View File

@@ -57,11 +57,14 @@ class EmailRenderer {
$subject = $this->replace_variables($template_settings['subject'], $variables);
$content = $this->replace_variables($template_settings['body'], $variables);
// Get HTML template design
$design_template = $this->get_design_template($template_settings['design'] ?? 'modern');
// 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($design_template, $content, $subject, $variables);
$html = $this->render_html($template_path, $content, $subject, $variables);
return [
'to' => $to,
@@ -204,6 +207,111 @@ class EmailRenderer {
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
*
@@ -222,18 +330,18 @@ class EmailRenderer {
/**
* Get design template path
*
* @param string $design Template name (modern, classic, minimal)
* @return string
*/
private function get_design_template($design) {
$template_path = WOONOOW_PATH . 'templates/emails/' . $design . '.html';
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, $design);
$template_path = apply_filters('woonoow_email_template', $template_path);
// Fallback to modern if template doesn't exist
// Fallback to base if custom template doesn't exist
if (!file_exists($template_path)) {
$template_path = WOONOOW_PATH . 'templates/emails/modern.html';
$template_path = WOONOOW_PATH . 'templates/emails/base.html';
}
return $template_path;
@@ -257,11 +365,69 @@ class EmailRenderer {
// 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_heading}}', $subject, $html);
$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('{{store_name}}', $variables['store_name'], $html);
$html = str_replace('{{store_url}}', $variables['store_url'], $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
@@ -271,4 +437,22 @@ class EmailRenderer {
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] ?? '';
}
}