Rewrite email template with inline styles for mobile compatibility
- All styles moved to inline attributes (no <style> block) - Table-based layout for Gmail/Outlook/mobile client compatibility - Aubade card effect via border-right/border-bottom:4px - Monospace font stack with web-safe fallbacks - btn() and quote() helper functions rewritten as <table> elements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+100
-38
@@ -6,35 +6,84 @@ function getResend() {
|
|||||||
const FROM = process.env.EMAIL_FROM ?? "noreply@mailsend.second-brain.ru";
|
const FROM = process.env.EMAIL_FROM ?? "noreply@mailsend.second-brain.ru";
|
||||||
const BASE_URL = process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru";
|
const BASE_URL = process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru";
|
||||||
|
|
||||||
|
// ── HTML template (inline styles for maximum email client compatibility) ───────
|
||||||
|
|
||||||
function base(content: string) {
|
function base(content: string) {
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<style>
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
body { margin: 0; padding: 0; background: #F5F5F0; font-family: 'Courier New', monospace; color: #323232; }
|
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
||||||
.wrap { max-width: 560px; margin: 40px auto; background: #F5F5F0; border: 2px solid #AAAAAA; box-shadow: 4px 4px 0 0 #AAAAAA; }
|
<title>Second Brain</title>
|
||||||
.header { padding: 24px 32px; border-bottom: 2px solid #AAAAAA; }
|
|
||||||
.header p { margin: 0; font-size: 14px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
|
|
||||||
.body { padding: 32px; }
|
|
||||||
.body p { margin: 0 0 16px; font-size: 14px; line-height: 1.6; }
|
|
||||||
.btn { display: inline-block; padding: 10px 24px; background: #E8F0D8; border: 2px solid #323232; box-shadow: 3px 3px 0 0 #323232; font-size: 13px; font-weight: 700; text-decoration: none; color: #323232; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
||||||
.footer { padding: 16px 32px; border-top: 2px solid #AAAAAA; }
|
|
||||||
.footer p { margin: 0; font-size: 11px; color: #AAAAAA; }
|
|
||||||
.tag { display: inline-block; padding: 2px 8px; border: 1px solid #AAAAAA; font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="margin:0;padding:0;background-color:#F5F5F0;font-family:'Courier New',Courier,monospace;color:#323232;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||||||
<div class="wrap">
|
|
||||||
<div class="header"><p>Second Brain · Образовательная платформа</p></div>
|
<!-- Outer wrapper -->
|
||||||
<div class="body">${content}</div>
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#F5F5F0;">
|
||||||
<div class="footer"><p>Это автоматическое письмо, не отвечайте на него.</p></div>
|
<tr>
|
||||||
</div>
|
<td align="center" style="padding:32px 16px;">
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:560px;background-color:#F5F5F0;border:2px solid #AAAAAA;border-right:4px solid #AAAAAA;border-bottom:4px solid #AAAAAA;">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:20px 28px;border-bottom:2px solid #AAAAAA;">
|
||||||
|
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:13px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#323232;">Second Brain · Образовательная платформа</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:28px 28px 24px;">
|
||||||
|
${content}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:14px 28px;border-top:2px solid #AAAAAA;">
|
||||||
|
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:11px;color:#AAAAAA;">Это автоматическое письмо, не отвечайте на него.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<!-- /Card -->
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reusable inline styles
|
||||||
|
const p = `style="margin:0 0 14px;font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1.65;color:#323232;"`;
|
||||||
|
const pLast = `style="margin:0;font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1.65;color:#323232;"`;
|
||||||
|
|
||||||
|
function btn(href: string, label: string) {
|
||||||
|
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin-top:24px;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#E8F0D8;border:2px solid #323232;border-right:4px solid #323232;border-bottom:4px solid #323232;">
|
||||||
|
<a href="${href}" target="_blank" style="display:inline-block;padding:10px 22px;font-family:'Courier New',Courier,monospace;font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#323232;text-decoration:none;">${label} →</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quote(text: string) {
|
||||||
|
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:16px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-left:3px solid #323232;padding:10px 14px;background-color:#E8F0D8;">
|
||||||
|
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:13px;line-height:1.6;color:#323232;">${text.replace(/\n/g, "<br/>")}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Email senders ─────────────────────────────────────────────────────────────
|
// ── Email senders ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function sendCourseAccessEmail(to: string, name: string, courseTitle: string) {
|
export async function sendCourseAccessEmail(to: string, name: string, courseTitle: string) {
|
||||||
@@ -43,10 +92,10 @@ export async function sendCourseAccessEmail(to: string, name: string, courseTitl
|
|||||||
to,
|
to,
|
||||||
subject: `Вам открыт доступ к курсу «${courseTitle}»`,
|
subject: `Вам открыт доступ к курсу «${courseTitle}»`,
|
||||||
html: base(`
|
html: base(`
|
||||||
<p>Привет, ${name}!</p>
|
<p ${p}>Привет, ${name}!</p>
|
||||||
<p>Вам открыт доступ к курсу <strong>«${courseTitle}»</strong>.</p>
|
<p ${p}>Вам открыт доступ к курсу <strong>«${courseTitle}»</strong>.</p>
|
||||||
<p style="margin-bottom: 24px;">Перейдите на платформу чтобы начать обучение:</p>
|
<p ${pLast}>Перейдите на платформу чтобы начать обучение:</p>
|
||||||
<a href="${BASE_URL}/dashboard" class="btn">Перейти к курсам →</a>
|
${btn(`${BASE_URL}/dashboard`, "Перейти к курсам")}
|
||||||
`),
|
`),
|
||||||
}).catch((e) => console.error("[email] sendCourseAccessEmail:", e));
|
}).catch((e) => console.error("[email] sendCourseAccessEmail:", e));
|
||||||
}
|
}
|
||||||
@@ -63,10 +112,10 @@ export async function sendHomeworkSubmittedEmail(
|
|||||||
to,
|
to,
|
||||||
subject: `Новая работа на проверку — ${lessonTitle}`,
|
subject: `Новая работа на проверку — ${lessonTitle}`,
|
||||||
html: base(`
|
html: base(`
|
||||||
<p>Привет, ${curatorName}!</p>
|
<p ${p}>Привет, ${curatorName}!</p>
|
||||||
<p>Студент <strong>${studentName}</strong> сдал работу по уроку <strong>«${lessonTitle}»</strong>.</p>
|
<p ${p}>Студент <strong>${studentName}</strong> сдал работу по уроку <strong>«${lessonTitle}»</strong>.</p>
|
||||||
<p style="margin-bottom: 24px;">Откройте работу чтобы проверить и оставить фидбек:</p>
|
<p ${pLast}>Откройте работу чтобы проверить и оставить фидбек:</p>
|
||||||
<a href="${BASE_URL}/curator/homework/${submissionId}" class="btn">Проверить работу →</a>
|
${btn(`${BASE_URL}/curator/homework/${submissionId}`, "Проверить работу")}
|
||||||
`),
|
`),
|
||||||
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
|
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
|
||||||
}
|
}
|
||||||
@@ -83,13 +132,11 @@ export async function sendFeedbackReceivedEmail(
|
|||||||
to,
|
to,
|
||||||
subject: `Получен фидбек по уроку «${lessonTitle}»`,
|
subject: `Получен фидбек по уроку «${lessonTitle}»`,
|
||||||
html: base(`
|
html: base(`
|
||||||
<p>Привет, ${studentName}!</p>
|
<p ${p}>Привет, ${studentName}!</p>
|
||||||
<p>Куратор проверил вашу работу по уроку <strong>«${lessonTitle}»</strong> и оставил обратную связь:</p>
|
<p ${p}>Куратор проверил вашу работу по уроку <strong>«${lessonTitle}»</strong> и оставил обратную связь:</p>
|
||||||
<div style="border-left: 3px solid #323232; padding: 12px 16px; margin: 16px 0; background: #E8F0D8; font-size: 13px; line-height: 1.6;">
|
${quote(feedbackText)}
|
||||||
${feedbackText.replace(/\n/g, "<br/>")}
|
<p ${pLast}>Вернитесь к уроку чтобы увидеть полный фидбек:</p>
|
||||||
</div>
|
${btn(lessonUrl, "Открыть урок")}
|
||||||
<p style="margin-bottom: 24px;">Вернитесь к уроку чтобы увидеть полный фидбек:</p>
|
|
||||||
<a href="${lessonUrl}" class="btn">Открыть урок →</a>
|
|
||||||
`),
|
`),
|
||||||
}).catch((e) => console.error("[email] sendFeedbackReceivedEmail:", e));
|
}).catch((e) => console.error("[email] sendFeedbackReceivedEmail:", e));
|
||||||
}
|
}
|
||||||
@@ -100,10 +147,25 @@ export async function sendWelcomeEmail(to: string, name: string) {
|
|||||||
to,
|
to,
|
||||||
subject: "Добро пожаловать в Second Brain",
|
subject: "Добро пожаловать в Second Brain",
|
||||||
html: base(`
|
html: base(`
|
||||||
<p>Привет, ${name}!</p>
|
<p ${p}>Привет, ${name}!</p>
|
||||||
<p>Ваш аккаунт на образовательной платформе <strong>Second Brain</strong> подтверждён.</p>
|
<p ${p}>Ваш аккаунт на образовательной платформе <strong>Second Brain</strong> подтверждён.</p>
|
||||||
<p style="margin-bottom: 24px;">После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
|
<p ${pLast}>После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
|
||||||
<a href="${BASE_URL}/dashboard" class="btn">Перейти на платформу →</a>
|
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")}
|
||||||
`),
|
`),
|
||||||
}).catch((e) => console.error("[email] sendWelcomeEmail:", e));
|
}).catch((e) => console.error("[email] sendWelcomeEmail:", e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendTestEmail(to: string) {
|
||||||
|
await getResend().emails.send({
|
||||||
|
from: FROM,
|
||||||
|
to,
|
||||||
|
subject: "Тест — Second Brain LMS",
|
||||||
|
html: base(`
|
||||||
|
<p ${p}>Привет!</p>
|
||||||
|
<p ${p}>Это тестовое письмо от платформы <strong>Second Brain LMS</strong>.</p>
|
||||||
|
<p ${p}>Так будут выглядеть все уведомления — доступ к курсу, фидбек по ДЗ и другие.</p>
|
||||||
|
<p ${pLast}>Если письмо отображается корректно, значит email-уведомления настроены и работают.</p>
|
||||||
|
${btn(`${BASE_URL}/dashboard`, "Открыть платформу")}
|
||||||
|
`),
|
||||||
|
}).catch((e) => console.error("[email] sendTestEmail:", e));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user