Add email notifications via Resend
- src/lib/email.ts: HTML templates for 4 email types (Second Brain design) - Welcome email on user registration (Better Auth databaseHooks) - Course access email when admin grants enrollment - Homework submitted email to all admins/curators (first submission only) - Feedback received email to student with feedback text and lesson link - Update TECHNICAL.md: Resend domain, from-address, email vars, Stage 3 summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { Resend } from "resend";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
const FROM = process.env.EMAIL_FROM ?? "noreply@mailsend.second-brain.ru";
|
||||
const BASE_URL = process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru";
|
||||
|
||||
function base(content: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; background: #F5F5F0; font-family: 'Courier New', monospace; color: #323232; }
|
||||
.wrap { max-width: 560px; margin: 40px auto; background: #F5F5F0; border: 2px solid #AAAAAA; box-shadow: 4px 4px 0 0 #AAAAAA; }
|
||||
.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>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="header"><p>Second Brain · Образовательная платформа</p></div>
|
||||
<div class="body">${content}</div>
|
||||
<div class="footer"><p>Это автоматическое письмо, не отвечайте на него.</p></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// ── Email senders ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function sendCourseAccessEmail(to: string, name: string, courseTitle: string) {
|
||||
await resend.emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
subject: `Вам открыт доступ к курсу «${courseTitle}»`,
|
||||
html: base(`
|
||||
<p>Привет, ${name}!</p>
|
||||
<p>Вам открыт доступ к курсу <strong>«${courseTitle}»</strong>.</p>
|
||||
<p style="margin-bottom: 24px;">Перейдите на платформу чтобы начать обучение:</p>
|
||||
<a href="${BASE_URL}/dashboard" class="btn">Перейти к курсам →</a>
|
||||
`),
|
||||
}).catch((e) => console.error("[email] sendCourseAccessEmail:", e));
|
||||
}
|
||||
|
||||
export async function sendHomeworkSubmittedEmail(
|
||||
to: string,
|
||||
curatorName: string,
|
||||
studentName: string,
|
||||
lessonTitle: string,
|
||||
submissionId: string
|
||||
) {
|
||||
await resend.emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
subject: `Новая работа на проверку — ${lessonTitle}`,
|
||||
html: base(`
|
||||
<p>Привет, ${curatorName}!</p>
|
||||
<p>Студент <strong>${studentName}</strong> сдал работу по уроку <strong>«${lessonTitle}»</strong>.</p>
|
||||
<p style="margin-bottom: 24px;">Откройте работу чтобы проверить и оставить фидбек:</p>
|
||||
<a href="${BASE_URL}/curator/homework/${submissionId}" class="btn">Проверить работу →</a>
|
||||
`),
|
||||
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
|
||||
}
|
||||
|
||||
export async function sendFeedbackReceivedEmail(
|
||||
to: string,
|
||||
studentName: string,
|
||||
lessonTitle: string,
|
||||
feedbackText: string,
|
||||
lessonUrl: string
|
||||
) {
|
||||
await resend.emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
subject: `Получен фидбек по уроку «${lessonTitle}»`,
|
||||
html: base(`
|
||||
<p>Привет, ${studentName}!</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;">
|
||||
${feedbackText.replace(/\n/g, "<br/>")}
|
||||
</div>
|
||||
<p style="margin-bottom: 24px;">Вернитесь к уроку чтобы увидеть полный фидбек:</p>
|
||||
<a href="${lessonUrl}" class="btn">Открыть урок →</a>
|
||||
`),
|
||||
}).catch((e) => console.error("[email] sendFeedbackReceivedEmail:", e));
|
||||
}
|
||||
|
||||
export async function sendWelcomeEmail(to: string, name: string) {
|
||||
await resend.emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
subject: "Добро пожаловать в Second Brain",
|
||||
html: base(`
|
||||
<p>Привет, ${name}!</p>
|
||||
<p>Ваш аккаунт на образовательной платформе <strong>Second Brain</strong> подтверждён.</p>
|
||||
<p style="margin-bottom: 24px;">После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.</p>
|
||||
<a href="${BASE_URL}/dashboard" class="btn">Перейти на платформу →</a>
|
||||
`),
|
||||
}).catch((e) => console.error("[email] sendWelcomeEmail:", e));
|
||||
}
|
||||
Reference in New Issue
Block a user