Add opt-in Daily Digest (daily email of last 24h of library recaps)
Multi-mode, off by default. Each new recap is synthesized into a 1-2 paragraph overview via the relay (operator-absorbed) and cached onto the session JSON; a daily 08:00 scan emails opted-in users their fresh recaps, deduped by a per-user watermark that never skips a failed or over-cap recap. One-click tokenized unsubscribe; settings-modal toggle; admin test trigger. Bumps to 0.2.158.
This commit is contained in:
@@ -167,6 +167,110 @@ export function renderSubscriptionReminderEmail({
|
||||
return { subject, text, html };
|
||||
}
|
||||
|
||||
// renderDigestEmail({ brandName, episodes, overflowCount, manageUrl,
|
||||
// unsubscribeUrl }) → { subject, text, html }
|
||||
// episodes: [{ title, type, url, overview }] — already capped + synthesized
|
||||
// by the scan. overflowCount: how many more are in the library beyond the
|
||||
// shown set (0 = none). Same minimal, spam-filter-friendly style as the
|
||||
// other emails: no images, inline CSS, one CTA. The unsubscribe link is a
|
||||
// one-click GET (no login) — required for deliverability + consent.
|
||||
export function renderDigestEmail({
|
||||
brandName = "Recaps",
|
||||
episodes = [],
|
||||
overflowCount = 0,
|
||||
manageUrl,
|
||||
unsubscribeUrl,
|
||||
}) {
|
||||
const n = episodes.length;
|
||||
const subject =
|
||||
n === 1
|
||||
? `Your ${brandName} digest: 1 new recap`
|
||||
: `Your ${brandName} digest: ${n} new recaps`;
|
||||
|
||||
const typeLabel = (t) =>
|
||||
t === "podcast" ? "Podcast" : t === "youtube" ? "Video" : "Recording";
|
||||
|
||||
const epText = episodes
|
||||
.map((ep) =>
|
||||
[
|
||||
`${ep.title || "Untitled"} (${typeLabel(ep.type)})`,
|
||||
ep.overview || "",
|
||||
ep.url || "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
const text = [
|
||||
`Here's what you added to ${brandName} in the last day:`,
|
||||
"",
|
||||
epText,
|
||||
"",
|
||||
overflowCount > 0
|
||||
? `…and ${overflowCount} more in your library: ${manageUrl}`
|
||||
: `Open your library: ${manageUrl}`,
|
||||
"",
|
||||
`You're receiving this because you turned on the daily digest. Unsubscribe: ${unsubscribeUrl}`,
|
||||
].join("\n");
|
||||
|
||||
const episodeBlocks = episodes
|
||||
.map((ep) => {
|
||||
const title = escapeHtml(ep.title || "Untitled");
|
||||
const titleHtml = ep.url
|
||||
? `<a href="${escapeAttr(ep.url)}" style="color:#111;text-decoration:none;">${title}</a>`
|
||||
: title;
|
||||
return `
|
||||
<tr>
|
||||
<td style="padding-bottom:20px;border-bottom:1px solid #eee;">
|
||||
<div style="font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:#999;padding-bottom:4px;">${escapeHtml(typeLabel(ep.type))}</div>
|
||||
<div style="font-size:16px;font-weight:600;color:#111;padding-bottom:8px;line-height:1.35;">${titleHtml}</div>
|
||||
<div style="font-size:14px;line-height:1.55;color:#444;">${escapeHtml(ep.overview || "")}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td style="height:20px;"></td></tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const overflowHtml =
|
||||
overflowCount > 0
|
||||
? `<tr><td style="font-size:13px;color:#888;padding-bottom:16px;">…and ${overflowCount} more in your library.</td></tr>`
|
||||
: "";
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<body style="margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fafafa;padding:32px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;padding:32px;max-width:90%;">
|
||||
<tr>
|
||||
<td style="font-size:18px;font-weight:600;color:#111;padding-bottom:20px;">
|
||||
Your ${escapeHtml(brandName)} digest
|
||||
</td>
|
||||
</tr>
|
||||
${episodeBlocks}
|
||||
${overflowHtml}
|
||||
<tr>
|
||||
<td align="center" style="padding:8px 0 24px;">
|
||||
<a href="${escapeAttr(manageUrl)}" style="display:inline-block;background:#111;color:#fff;text-decoration:none;font-size:15px;font-weight:500;padding:12px 24px;border-radius:6px;">Open your library</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:12px;line-height:1.5;color:#888;border-top:1px solid #eee;padding-top:16px;">
|
||||
You're receiving this because you turned on the daily digest. <a href="${escapeAttr(unsubscribeUrl)}" style="color:#888;">Unsubscribe</a> anytime, or manage it in Settings.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return { subject, text, html };
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
|
||||
Reference in New Issue
Block a user