Clean email templates (Linear/Resend style) + LiteLLM Gemini STT
Emails: white card, clean typography, dark button, no gradients. Same minimal aesthetic as Linear/Resend/Notion emails. Verify page responses also updated to match.
This commit is contained in:
parent
bb459f57b8
commit
ce8e959ea4
2 changed files with 69 additions and 39 deletions
|
|
@ -66,11 +66,12 @@ router.post('/config/test-email', async function(req, res) {
|
|||
}
|
||||
|
||||
var emailWrapper = require('./auth').__emailWrapper;
|
||||
var btnStyle = require('./auth').__btnStyle;
|
||||
var btnHtml = require('./auth').__btnHtml;
|
||||
|
||||
var html = emailWrapper(
|
||||
'<p style="color:#4b5563;margin:0 0 20px;line-height:1.6;">' + bodyText.replace(/\n/g, '<br>') + '</p>' +
|
||||
(btnStyle ? '<a href="#" style="' + btnStyle() + '">Test Button</a>' : '')
|
||||
'<p style="margin:0 0 6px;font-size:18px;font-weight:600;color:#111827;">Test Email</p>' +
|
||||
'<p style="color:#6b7280;margin:12px 0 20px;line-height:1.6;font-size:14px;">' + bodyText.replace(/\n/g, '<br>') + '</p>' +
|
||||
btnHtml('#', 'Test Button')
|
||||
);
|
||||
|
||||
var ok = await sendEmail(to, '[TEST] ' + subject, html);
|
||||
|
|
|
|||
|
|
@ -38,49 +38,78 @@ function safeAppUrl() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// EMAIL TEMPLATES — responsive, max-width constrained
|
||||
// EMAIL TEMPLATES — clean, minimal (Linear / Resend style)
|
||||
// ============================================================
|
||||
function emailWrapper(body) {
|
||||
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<style>
|
||||
body,table,td{font-family:'Segoe UI',Roboto,Arial,sans-serif;}
|
||||
@media only screen and (max-width:600px){
|
||||
.email-outer{padding:16px 8px !important;}
|
||||
.email-inner{width:100% !important;min-width:0 !important;}
|
||||
.email-body{padding:24px 20px !important;}
|
||||
.email-header{padding:20px 20px !important;}
|
||||
.email-footer{padding:12px 20px 20px !important;}
|
||||
.email-btn{padding:12px 24px !important;font-size:14px !important;}
|
||||
}
|
||||
</style>
|
||||
var siteName = process.env.SITE_NAME || 'Pediatric AI Scribe';
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>${escHtml(siteName)}</title>
|
||||
<style>
|
||||
body,table,td,p,a{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;}
|
||||
@media only screen and (max-width:600px){
|
||||
.wrap{width:100% !important;padding:0 16px !important;}
|
||||
.inner{padding:28px 24px !important;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f3f4f6;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f3f4f6;">
|
||||
<tr><td class="email-outer" style="padding:32px 16px;" align="center">
|
||||
<table role="presentation" class="email-inner" cellpadding="0" cellspacing="0" border="0" style="background:white;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);max-width:520px;width:100%;">
|
||||
<tr><td class="email-header" style="background:linear-gradient(135deg,#2563eb 0%,#7c3aed 100%);padding:24px 32px;">
|
||||
<p style="margin:0;color:white;font-size:20px;font-weight:700;">Pediatric AI Scribe</p>
|
||||
<p style="margin:4px 0 0;color:rgba(255,255,255,0.8);font-size:12px;">AI-Powered Clinical Documentation</p>
|
||||
</td></tr>
|
||||
<tr><td class="email-body" style="padding:32px;">${body}</td></tr>
|
||||
<tr><td class="email-footer" style="padding:14px 32px 24px;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;color:#9ca3af;font-size:11px;line-height:1.5;">This email was sent by Pediatric AI Scribe. If you did not request this, you can safely ignore it.</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
<body style="margin:0;padding:0;background:#fafafa;-webkit-text-size-adjust:100%;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fafafa;">
|
||||
<tr><td align="center" style="padding:40px 24px;">
|
||||
|
||||
<!-- Logo / wordmark -->
|
||||
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;margin-bottom:24px;">
|
||||
<tr><td>
|
||||
<p style="margin:0;font-size:15px;font-weight:600;color:#111827;letter-spacing:-0.2px;">${escHtml(siteName)}</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- Card -->
|
||||
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;background:#ffffff;border:1px solid #e5e7eb;border-radius:8px;">
|
||||
<tr><td class="inner" style="padding:36px 40px;">
|
||||
${body}
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" class="wrap" cellpadding="0" cellspacing="0" border="0" style="max-width:520px;width:100%;margin-top:20px;">
|
||||
<tr><td>
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.6;">
|
||||
${escHtml(siteName)} · AI-powered clinical documentation<br>
|
||||
If you didn’t request this email, you can safely ignore it.
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function btnHtml(href, label) {
|
||||
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px auto;"><tr><td align="center" style="border-radius:8px;background:linear-gradient(135deg,#2563eb,#7c3aed);">
|
||||
<a class="email-btn" href="${escHtml(href)}" target="_blank" style="display:inline-block;color:white;padding:13px 32px;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px;font-family:'Segoe UI',Roboto,Arial,sans-serif;">${escHtml(label)}</a>
|
||||
</td></tr></table>`;
|
||||
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:28px 0 4px;">
|
||||
<tr><td style="border-radius:6px;background:#111827;">
|
||||
<a href="${escHtml(href)}" target="_blank"
|
||||
style="display:inline-block;color:#ffffff;padding:12px 24px;border-radius:6px;
|
||||
text-decoration:none;font-weight:500;font-size:14px;
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
|
||||
letter-spacing:-0.1px;">${escHtml(label)}</a>
|
||||
</td></tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function linkFallback(url) {
|
||||
return `<p style="color:#6b7280;font-size:12px;margin:16px 0 4px;">Button not working? Copy this link into your browser:</p>
|
||||
<p style="background:#f3f4f6;border-radius:6px;padding:10px 14px;font-size:11px;word-break:break-all;color:#374151;margin:0;max-width:100%;overflow-wrap:break-word;">${escHtml(url)}</p>`;
|
||||
return `<p style="margin:20px 0 6px;font-size:13px;color:#6b7280;">
|
||||
Or copy and paste this link into your browser:
|
||||
</p>
|
||||
<p style="margin:0;background:#f3f4f6;border-radius:4px;padding:10px 12px;
|
||||
font-size:12px;font-family:'SFMono-Regular',Consolas,monospace;
|
||||
word-break:break-all;color:#374151;line-height:1.5;">${escHtml(url)}</p>`;
|
||||
}
|
||||
|
||||
// Email helper — DB settings override env vars
|
||||
|
|
@ -183,11 +212,11 @@ router.get('/verify-email', async (req, res) => {
|
|||
if (!token) return res.status(400).send('Missing token');
|
||||
var user = await db.get('SELECT id, name FROM users WHERE verify_token = ? AND verify_expires > ?', [token, Date.now()]);
|
||||
if (!user) {
|
||||
return res.send('<html><body style="font-family:sans-serif;text-align:center;padding:60px;"><h2 style="color:#ef4444;">Invalid or Expired Link</h2><p><a href="' + safeAppUrl() + '">Go to app</a></p></body></html>');
|
||||
return res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Link Expired</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Link expired or invalid</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">This verification link has expired. Request a new one from the app.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Go to app</a></body></html>');
|
||||
}
|
||||
await db.run('UPDATE users SET email_verified = true, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]);
|
||||
await db.run('INSERT INTO audit_log (user_id, action) VALUES (?, ?)', [user.id, 'email_verified']);
|
||||
res.send('<html><body style="font-family:sans-serif;text-align:center;padding:60px;"><h2 style="color:#10b981;">Email Verified!</h2><p>Welcome, ' + escHtml(user.name) + '!</p><p style="margin-top:20px;"><a href="' + safeAppUrl() + '" style="background:#2563eb;color:white;padding:12px 30px;border-radius:8px;text-decoration:none;">Open App</a></p></body></html>');
|
||||
res.send('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Email Verified</title></head><body style="margin:0;padding:40px 24px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;background:#fafafa;text-align:center;"><p style="font-size:15px;font-weight:600;color:#111827;margin:0 0 8px;">Email verified</p><p style="color:#6b7280;font-size:14px;margin:0 0 24px;">Welcome, ' + escHtml(user.name) + '. You\'re all set.</p><a href="' + safeAppUrl() + '" style="display:inline-block;background:#111827;color:#fff;padding:10px 22px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:500;">Open app</a></body></html>');
|
||||
} catch (err) { console.error('[Auth] Verify error:', err.message); res.status(500).send('Verification failed. Please try again.'); }
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue