From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind

---
 inc/managers/EmailManager.php |  382 ++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 324 insertions(+), 58 deletions(-)

diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index c1e5202..106a387 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -24,7 +24,7 @@
 class EmailManager
 {
 
-   private array $colours = JVB_EMAIL['colours'];
+   public array $colours = JVB_EMAIL['colours'];
    private string $title = JVB_EMAIL['content']['title'];
    private string $prefix = JVB_EMAIL['content']['subjectPrefix'];
    private string $signature = JVB_EMAIL['content']['signature'];
@@ -82,7 +82,7 @@
      * @param string $header Optional header text for the template
      * @return bool Whether the email was sent successfully
      */
-    public function sendEmail(string $to, string $subject, string $message, string $header = ''):bool
+    public function sendEmail(string $to, string $subject, string $message, string $header = '', array $headers = [], array $attachments = []):bool
     {
         // Make sure the content type is set to HTML
         add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
@@ -91,7 +91,7 @@
         $formatted_message = $this->getEmailTemplate($message, $header);
 
         // Send the email
-        $result = wp_mail($to, $subject, $formatted_message);
+        $result = wp_mail($to, $subject, $formatted_message, $headers, $attachments);
 
         // Reset content type filter to avoid affecting other emails
         remove_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
@@ -105,13 +105,17 @@
      *
      * @return string
      */
-    private function getEmailTemplate(string $content, string $header = ''):string
+    private function getEmailTemplate(string $content, string $headerText = ''):string
     {
-        $logo = get_custom_logo();
+		$custom_logo_id = get_theme_mod( 'custom_logo' );
+		$logo_thumbnail = wp_get_attachment_image_src( $custom_logo_id, 'custom-logo-thumbnail' );
+
+		$logo = ($logo_thumbnail) ? '<img src="' . esc_url( $logo_thumbnail[0] ) . '" alt="Site Logo" width="150" height="150" style="max-width:120px;height:auto;">' : '';
+
 
         // Default header if none provided
-        if (empty($header)) {
-            $header = $this->title;
+        if (empty($headerText)) {
+			$headerText = $this->title;
         }
 
         return '<!DOCTYPE html>
@@ -120,7 +124,28 @@
             <meta charset="UTF-8">
             <meta name="viewport" content="width=device-width, initial-scale=1.0">
             <title>' . $this->title . ' | ' . $this->site_name .'</title>
-            <style>
+        </head>
+        <body style="font-family:Segoe UI, Tahoma, Geneva, Verdana, sans-serif;background-color: '.$this->colours['light'].';color:'.$this->colours['dark'].';line-height:1.5;margin:0;padding:0;">
+			<div style="background-color:'.$this->colours['dark'].';color:'.$this->colours['light'].';padding:5px;text-align:center;font-size:24px;font-weight:900;letter-spacing:1.5px;text-transform:uppercase;border-radius:5px 5px 0 0;">
+				<a href="' . $this->site_url . '" style="display:inline-block;vertical-align:middle;">
+					' . $logo . '
+				</a>
+				<p style="display:inline-block;vertical-align:middle;margin: 0 auto 0 0;">' . $headerText . '</p>
+			</div>
+			<div style="padding:4rem 0;width:100%;max-width:600px;margin:0 auto;">
+				' . $content . '
+				'.$this->signature().'
+			</div>
+			<div style="border-radius:0 0 5px 5px;text-align:center;margin-top:20px;font-size:12px;background-color:'.$this->colours['light-200'].'; color: '.$this->colours['dark-200'].';padding:10px;line-height:1.6;">
+				' . $this->footer . '
+			</div>
+        </body>
+        </html>';
+    }
+
+	private function oldStyle(){
+		$oldStyle ='
+		<style>
                 body, table, td, p, a, li, blockquote {
                     line-height: 1.5;
                 }
@@ -161,8 +186,7 @@
                     vertical-align: middle;
                 }
                 .header p {
-                    margin: 0;
-                    margin-left: auto;
+                    margin: 0 auto 0 0;
                 }
 
                 .content {
@@ -194,12 +218,24 @@
                     text-align: center;
                     margin-top: 20px;
                     font-size: 12px;
-                    backgorund-color: ' . $this->colours['light-200'] . ';
+                    background-color: ' . $this->colours['light-200'] . ';
                     color: ' . $this->colours['dark-200'] . ';
                     padding: 10px;
                     line-height: 1.6;
                 }
 
+                .notice {
+                	text-align: center;
+                	border-radius: 0 8px 8px 0;
+                	margin: 1rem 0;
+                	border-left: 4px solid '.$this->colours['action-0'].';
+                	padding: 1rem;
+                	background-color: '.$this->colours['light-100'].';
+                }
+                .notice strong {
+                	color: '.$this->colours['action-0'].';
+                }
+
                 .divider {
                     border-top: 1px solid ' . $this->colours['dark-200'] . ';
                     margin: 25px 0;
@@ -300,26 +336,8 @@
     }
 }
             </style>
-        </head>
-        <body>
-            <div class="email-container">
-
-                <div class="header">
-                    <a href="' . $this->site_url . '">
-                        ' . $logo . '
-                    </a>
-                    <p>' . $header . '</p>
-                </div>
-                <div class="content">
-                    ' . $content . '
-                </div>
-                <div class="footer">
-                    ' . $this->footer . '
-                </div>
-            </div>
-        </body>
-        </html>';
-    }
+		';
+	}
 
     /**
      * New user registration email to user
@@ -336,8 +354,6 @@
 			$user->user_login,
 		);
 		$message = apply_filters('jvbNewUserEmail', $message, $user);
-
-		$message .= $this->signature;
 		$wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['types']['newUser']['subject']?:'New User');
 
         // Change the subject line
@@ -362,7 +378,6 @@
         $message .= '<p><strong>Username:</strong> ' . $user->user_login . '</p>';
         $message .= '<p><strong>Email:</strong> ' . $user->user_email . '</p>';
 		$message = apply_filters('jvbNewUserAdminEmail', $message, $user);
-        $message .= $this->signature;
         $emailData['message'] = $this->getEmailTemplate($message, 'New User Registration');
         $emailData['subject'] = $this->prefix .'New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name;
 
@@ -390,15 +405,15 @@
 			%s
 			<p>Or copy and paste this link into your browser:</p>
 			%s
-			<div class="divider"></div>
+			%s
 			<p>This password reset link is only valid for 24 hours.</p>',
 			$user->display_name,
 			$user_login,
-			JVB()->email()->button($reset_url,'Reset Password'),
-			JVB()->email()->link($reset_url)
+			$this->button($reset_url,'Reset Password'),
+			$this->link($reset_url),
+			$this->divider()
 		);
 		$content = apply_filters('jvbPasswordResetEmail', $content, $user_login, $user, $reset_url);
-		$content .= $this->signature;
         return $this->getEmailTemplate($content, 'Password Reset');
     }
 
@@ -432,16 +447,16 @@
         <p>Ideally you already know this: someone asked to change the email for your account.</p>
         <p><strong>Old Email:</strong> %s</p>
         <p><strong>New Email:</strong> %s</p>
-        <div class="divider"></div>
+        %s
         <p>If this is news to you, or you did not request this - please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>
         %s',
 			$newUser['first_name'],
 			$oldUser['user_email'],
 			$newUser['user_email'],
-			JVB()->email()->button(wp_login_url(), 'Log In To Your Account')
+			$this->divider(),
+			$this->button(wp_login_url(), 'Log In To Your Account')
 		);
 		$content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser);
-        $content .= $this->signature;
         $email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed');
 		$prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
@@ -469,15 +484,14 @@
 			%s
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
-			JVB()->email()->button($confirm_url, 'Confirm this Email'),
-			JVB()->email()->link($confirm_url)
+			$this->button($confirm_url, 'Confirm this Email'),
+			$this->link($confirm_url)
 		);
 
 		$content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url);
 
-        $content .= '<div class="divider"></div>';
+        $content .= $this->divider();
         $content .= '<p>If you did not request this change, you can safely ignore this email and nothing will change.</p>';
-        $content .= $this->signature;
 
         return $this->getEmailTemplate($content, 'Confirm Email Change');
     }
@@ -499,10 +513,9 @@
 			<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>
 			%s',
 			$oldUser['first_name'],
-			JVB()->email()->button(wp_login_url(), 'Log In to Your Account')
+			$this->button(wp_login_url(), 'Log In to Your Account')
 		);
 		$content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser);
-        $content .= $this->signature;
 
         $pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed');
 		$prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true;
@@ -545,14 +558,13 @@
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
 			$request_name,
-			JVB()->email()->button($confirm_url, 'Confirm'),
-			JVB()->email()->link($confirm_url)
+			$this->button($confirm_url, 'Confirm'),
+			$this->link($confirm_url)
 		);
 		$message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data);
 
-        $message .= '<div class="divider"></div>';
+        $message .= $this->divider();
         $message .= '<p>If you did not make this request, you can safely ignore this email.</p>';
-        $message .= $this->signature;
 
         return $this->getEmailTemplate($message, 'Action Confirmation');
     }
@@ -577,28 +589,30 @@
 			%s
 			<p>Or you can copy and paste this link into your browser:</p>
 			%s
-			<div class="divider"></div>
+			%s
 			<p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>',
-			JVB()->email()->button($download_url, 'Download Your Data'),
-			JVB()->email()->link($download_url),
+			$this->button($download_url, 'Download Your Data'),
+			$this->link($download_url),
+			$this->divider(),
 			$expiresAt
 		);
 		$message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data);
-        $message .= $this->signature;
 
         return $this->getEmailTemplate($message, 'Your Personal Data Export');
     }
 
 	public function signature():string
 	{
-		return $this->signature;
+		return '<p><i>'.$this->signature.'</i></p>';
 	}
 
 	public function button(string $link, string $title):string
 	{
 		return sprintf(
-			'<p style="text-align: center;"><a href="%s" class="button">%s</a></p>',
+			'<p style="text-align: center;"><a href="%s" style="display:inline-block;padding:16px 10px;border-radius:4px;background-color:%s;color:%s;text-decoration:none;font-weight:bold;margin:15px 0;letter-spacing:1px;text-transform:uppercase;">%s</a></p>',
 			$link,
+			$this->colours['action-0'],
+			$this->colours['action-contrast'],
 			$title
 		);
 	}
@@ -606,11 +620,263 @@
 	public function link(string $link):string
 	{
 		return sprintf(
-			'<p style="user-select:all;">%s</p>',
+			'<code style="color:%s;border:1px solid %s;background-color:%s;border-radius:4px;user-select:all;">%s</code>',
+			$this->colours['dark-200'],
+			$this->colours['dark-100'],
+			$this->colours['light-100'],
 			$link
 		);
 	}
 
+	public function divider():string
+	{
+		return '<div style="border-top:1px solid '.$this->colours['dark-200'].';margin:25px 0;"></div>';
+	}
+
+	public function notice(string $text):string
+	{
+		return '<div style="border-radius: 0 8px 8px 0; margin: 1rem 0; border-left: 4px solid '.$this->colours['action-0'].';padding:1rem;background-color:'.$this->colours['light-100'].';">
+		'.str_replace('<strong>', '<strong style="color:'.$this->colours['action-0'].'">',$text).'
+		</div>';
+	}
+
+	public function callout(string $text):string
+	{
+		return sprintf(
+			'<div style="padding:2rem;margin:2rem 3rem;background-color:%s;color:%s;">%s</div>',
+			$this->colours['action-0'],
+			$this->colours['action-contrast'],
+			str_replace('<a', '<a style="background-color:'.$this->colours['action-contrast'].';padding: 0 .125rem;border-radius:4px;"', $text)
+		);
+	}
+
+	public function table(array $summarize, string $title = '', array $actions = []):string
+	{
+		if (empty($summarize)){
+			return '';
+		}
+
+		if (!empty($title)) {
+			$title = sprintf(
+			'<h2 style="color:%s;border-bottom:2px solid %s;padding-bottom:15px;margin-bottom:20px;text-align:center;">%s</h2>',
+			$this->colours['dark-200'],
+			$this->colours['dark-200'],
+			$title
+			);
+		}
+		$content = '';
+		foreach ($summarize as $index=> $item) {
+			if (!array_key_exists('label', $item) && !array_key_exists('value', $item)) {
+				continue;
+			}
+			$content .= sprintf(
+				'<div style="padding:10px 0;border-bottom:1px solid %s;background-color:%s;">
+					<span style="display:inline-block;vertical-align:top;font-weight:600;color:%s;width:%s;">%s</span>
+					<div style="display:inline-block;vertical-align:top;width:%s;">%s</div>
+				</div>',
+				$this->colours['dark-200'],
+				($index%2 === 0) ? $this->colours['light-100'] : $this->colours['light-50'],
+				$this->colours['dark-200'],
+				'19%',
+				$item['label'],
+				'80%',
+				$item['value']
+			);
+		}
+
+		return sprintf(
+			'<div style="max-width:500px;margin:40px auto;padding:30px;background-color:%s;border-radius:10px;border:2px dashed %s;">%s%s</div>',
+			$this->colours['light-100'],
+			$this->colours['dark-200'],
+			$title,
+			$content
+		);
+	}
+
+	public function card(string $content, string $title = ''):string
+	{
+		$titleHtml = '';
+		if (!empty($title)) {
+			$titleHtml = sprintf(
+				'<h3 style="margin:0 0 15px 0;color:%s;font-size:18px;font-weight:600;">%s</h3>',
+				$this->colours['dark'],
+				esc_html($title)
+			);
+		}
+
+		return sprintf(
+			'<div style="background-color:%s;border:1px solid %s;border-radius:8px;padding:20px;margin:15px 0;">%s%s</div>',
+			$this->colours['light-50'],
+			$this->colours['light-200'],
+			$titleHtml,
+			$content
+		);
+	}
+
+	public function stat(string $number, string $label, string $description = ''):string
+	{
+		$desc = $description ? sprintf(
+			'<p style="margin:5px 0 0 0;font-size:12px;color:%s;">%s</p>',
+			$this->colours['dark-200'],
+			esc_html($description)
+		) : '';
+
+		return sprintf(
+			'<div style="text-align:center;padding:20px;background-color:%s;border-radius:8px;margin:10px 0;">
+            <div style="font-size:36px;font-weight:700;color:%s;margin:0 0 5px 0;">%s</div>
+            <div style="font-size:14px;font-weight:600;color:%s;text-transform:uppercase;letter-spacing:1px;">%s</div>
+            %s
+        </div>',
+			$this->colours['light-100'],
+			$this->colours['action-0'],
+			esc_html($number),
+			$this->colours['dark-200'],
+			esc_html($label),
+			$desc
+		);
+	}
+
+	public function grid(array $items, int $columns = 2):string
+	{
+		$width = floor(100 / $columns) - 2; // 2% gap
+
+		$html = '<div style="display:table;width:100%;margin:20px 0;">';
+		foreach ($items as $index => $item) {
+			if ($index > 0 && $index % $columns === 0) {
+				$html .= '</div><div style="display:table;width:100%;margin:20px 0;">';
+			}
+			$html .= sprintf(
+				'<div style="display:table-cell;width:%d%%;padding:10px;vertical-align:top;">%s</div>',
+				$width,
+				$item
+			);
+		}
+		$html .= '</div>';
+
+		return $html;
+	}
+
+	public function image(string $src, string $alt = '', int $maxWidth = 600):string
+	{
+		return sprintf(
+			'<div style="text-align:center;margin:20px 0;">
+            <img src="%s" alt="%s" style="max-width:%dpx;width:100%%;height:auto;border-radius:8px;" />
+        </div>',
+			esc_url($src),
+			esc_attr($alt),
+			$maxWidth
+		);
+	}
+
+	public function h1(string $text):string
+	{
+		return sprintf(
+			'<h1 style="color:%s;font-size:24px;font-weight:700;margin:20px 0 10px 0;">%s</h1>',
+			$this->colours['dark'],
+			$text
+		);
+	}
+
+	public function h2(string $text):string
+	{
+		return sprintf(
+			'<h2 style="color:%s;font-size:20px;font-weight:600;margin:20px 0 10px 0;border-bottom:2px solid %s;padding-bottom:10px;">%s</h2>',
+			$this->colours['dark'],
+			$this->colours['dark-200'],
+			$text
+		);
+	}
+
+	public function h3(string $text):string
+	{
+		return sprintf(
+			'<h3 style="color:%s;font-size:18px;font-weight:600;margin:15px 0 10px 0;">%s</h3>',
+			$this->colours['dark-200'],
+			$text
+		);
+	}
+
+	public function list(array $items, bool $ordered = false):string
+	{
+		$tag = $ordered ? 'ol' : 'ul';
+		$style = $ordered
+			? 'list-style-type:decimal;padding-left:20px;margin:10px 0;'
+			: 'list-style-type:disc;padding-left:20px;margin:10px 0;';
+
+		$html = sprintf('<%s style="%s">', $tag, $style);
+		foreach ($items as $item) {
+			$html .= sprintf(
+				'<li style="margin:5px 0;line-height:1.6;">%s</li>',
+				$item
+			);
+		}
+		$html .= sprintf('</%s>', $tag);
+
+		return $html;
+	}
+
+	public function codeBlock(string $code, string $language = ''):string
+	{
+		return sprintf(
+			'<pre style="background-color:%s;border:1px solid %s;border-radius:4px;padding:15px;overflow-x:auto;font-family:\'Courier New\',monospace;font-size:13px;line-height:1.4;"><code>%s</code></pre>',
+			$this->colours['light-100'],
+			$this->colours['dark-200'],
+			esc_html($code)
+		);
+	}
+
+	public function badge(string $text, string $type = 'default'):string
+	{
+		$colors = [
+			'success' => ['bg' => '#d4edda', 'text' => '#155724'],
+			'warning' => ['bg' => '#fff3cd', 'text' => '#856404'],
+			'error' => ['bg' => '#f8d7da', 'text' => '#721c24'],
+			'info' => ['bg' => '#d1ecf1', 'text' => '#0c5460'],
+			'default' => ['bg' => $this->colours['light-200'], 'text' => $this->colours['dark-200']]
+		];
+
+		$color = $colors[$type] ?? $colors['default'];
+
+		return sprintf(
+			'<span style="display:inline-block;padding:4px 12px;border-radius:12px;background-color:%s;color:%s;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">%s</span>',
+			$color['bg'],
+			$color['text'],
+			esc_html($text)
+		);
+	}
+
+	public function alert(string $text, string $type = 'info'):string
+	{
+		$configs = [
+			'success' => ['bg' => '#d4edda', 'border' => '#28a745', 'text' => '#155724', 'icon' => '✓'],
+			'warning' => ['bg' => '#fff3cd', 'border' => '#ffc107', 'text' => '#856404', 'icon' => '⚠'],
+			'error' => ['bg' => '#f8d7da', 'border' => '#dc3545', 'text' => '#721c24', 'icon' => '✕'],
+			'info' => ['bg' => '#d1ecf1', 'border' => $this->colours['action-0'], 'text' => '#0c5460', 'icon' => 'ℹ']
+		];
+
+		$config = $configs[$type] ?? $configs['info'];
+
+		return sprintf(
+			'<div style="border-radius:8px;margin:1rem 0;border-left:4px solid %s;padding:1rem;background-color:%s;color:%s;">
+            <strong style="font-size:16px;">%s</strong> %s
+        </div>',
+			$config['border'],
+			$config['bg'],
+			$config['text'],
+			$config['icon'],
+			$text
+		);
+	}
+
+	public function spacer(int $height = 20):string
+	{
+		return sprintf('<div style="height:%dpx;"></div>', $height);
+	}
+
+	public function prefix():string
+	{
+		return $this->prefix;
+	}
 }
 
 

--
Gitblit v1.10.0