Send the invitation as a light letter that names the community, the inviter by email, and who can read the Rule.
This commit is contained in:
1 parent
70ceb73104
commit
727a92a9ac
14 files changed
+534
-26
No files matched your search
@@ -0,0 +1,249 @@
|
||||
import { ASSETS, getAssetPath } from "../assetUtils";
|
||||
import {
|
||||
clipCommunityInviteDescription,
|
||||
communityInviteInitials,
|
||||
communityPossessive,
|
||||
inviteRuleCountsLabel,
|
||||
} from "../create/ruleInviteCardMeta";
|
||||
|
||||
export const RULE_STAKEHOLDER_INVITE_LOGO_PATH = getAssetPath(ASSETS.LOGO_MARK);
|
||||
|
||||
const PREVIEW_TEXT =
|
||||
"It covers how they talk, who belongs, how they decide and what happens when they disagree.";
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
export function ruleStakeholderInviteCopyUrl(verifyUrl: string): string {
|
||||
return `${verifyUrl}${verifyUrl.includes("?") ? "&" : "?"}copy=1`;
|
||||
}
|
||||
|
||||
export type RuleStakeholderInviteMailInput = {
|
||||
to: string;
|
||||
verifyUrl: string;
|
||||
origin: string;
|
||||
communityName: string;
|
||||
inviterEmail: string;
|
||||
summary?: string | null;
|
||||
document?: unknown;
|
||||
};
|
||||
|
||||
export type RuleStakeholderInviteMailContent = {
|
||||
subject: string;
|
||||
previewText: string;
|
||||
text: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export function buildRuleStakeholderInviteMail(
|
||||
input: RuleStakeholderInviteMailInput,
|
||||
): RuleStakeholderInviteMailContent {
|
||||
const communityName = input.communityName.trim() || "A community";
|
||||
const inviterEmail = input.inviterEmail.trim().toLowerCase();
|
||||
const origin = input.origin.replace(/\/$/, "");
|
||||
const verifyUrl = input.verifyUrl;
|
||||
const copyUrl = ruleStakeholderInviteCopyUrl(verifyUrl);
|
||||
const possessive = communityPossessive(communityName);
|
||||
const description = clipCommunityInviteDescription(input.summary);
|
||||
const initials = communityInviteInitials(communityName);
|
||||
const countsLabel = inviteRuleCountsLabel(input.document);
|
||||
const subject = `${communityName} invited you to read their Rule`;
|
||||
const intro = `${inviterEmail} sent the invitation. Their Rule sets out how they talk, who belongs, how they decide and what happens when they disagree.`;
|
||||
const privacy = `Every Rule starts private. Only the people ${communityName} invites can read it until the person who holds the pen turns on a link. A Rule is public and featured only when a community asks us in writing.`;
|
||||
const ownLink =
|
||||
"Your own link. Forwarding this email shares it. Ask the community to invite anyone else who should read the Rule.";
|
||||
const copyLead =
|
||||
"Writing a Rule for another community? Make a copy of this one and change what does not fit.";
|
||||
const why = `You got this email because ${inviterEmail} added your address to ${possessive} Rule on CommunityRule. If you do not know the community, you can ignore this email.`;
|
||||
const madeBy =
|
||||
"CommunityRule is made by the Media Economies Design Lab at the University of Colorado Boulder.";
|
||||
const privacyUrl = `${origin}/privacy`;
|
||||
const aboutUrl = `${origin}/about`;
|
||||
const logoUrl = `${origin}${RULE_STAKEHOLDER_INVITE_LOGO_PATH}`;
|
||||
|
||||
const text = [
|
||||
subject,
|
||||
"",
|
||||
intro,
|
||||
"",
|
||||
"Read the Rule",
|
||||
verifyUrl,
|
||||
"",
|
||||
privacy,
|
||||
"",
|
||||
ownLink,
|
||||
"",
|
||||
copyLead,
|
||||
copyUrl,
|
||||
"",
|
||||
why,
|
||||
"",
|
||||
madeBy,
|
||||
`Privacy ${privacyUrl}`,
|
||||
].join("\n");
|
||||
|
||||
const html = inviteHtml({
|
||||
subject,
|
||||
intro,
|
||||
verifyUrl,
|
||||
copyUrl,
|
||||
communityName,
|
||||
description,
|
||||
initials,
|
||||
countsLabel,
|
||||
privacy,
|
||||
ownLink,
|
||||
copyLead,
|
||||
why,
|
||||
madeBy,
|
||||
privacyUrl,
|
||||
aboutUrl,
|
||||
logoUrl,
|
||||
});
|
||||
|
||||
return { subject, previewText: PREVIEW_TEXT, text, html };
|
||||
}
|
||||
|
||||
function inviteHtml(opts: {
|
||||
subject: string;
|
||||
intro: string;
|
||||
verifyUrl: string;
|
||||
copyUrl: string;
|
||||
communityName: string;
|
||||
description: string;
|
||||
initials: string;
|
||||
countsLabel: string;
|
||||
privacy: string;
|
||||
ownLink: string;
|
||||
copyLead: string;
|
||||
why: string;
|
||||
madeBy: string;
|
||||
privacyUrl: string;
|
||||
aboutUrl: string;
|
||||
logoUrl: string;
|
||||
}): string {
|
||||
const heading = escapeHtml(opts.subject);
|
||||
const intro = escapeHtml(opts.intro);
|
||||
const verifyHref = escapeHtml(opts.verifyUrl);
|
||||
const copyHref = escapeHtml(opts.copyUrl);
|
||||
const name = escapeHtml(opts.communityName);
|
||||
const description = escapeHtml(opts.description);
|
||||
const initials = escapeHtml(opts.initials);
|
||||
const counts = escapeHtml(opts.countsLabel);
|
||||
const privacyHtml = privacyHtmlWithLead(opts.privacy);
|
||||
const ownLinkHtml = leadInHtml("Your own link.", opts.ownLink);
|
||||
const copyHtml =
|
||||
`<strong style="color:#000000;">Writing a Rule for another community?</strong>` +
|
||||
` <a href="${copyHref}" style="color:#000000;text-decoration:underline;">Make a copy</a>` +
|
||||
` of this one and change what does not fit.`;
|
||||
const why = escapeHtml(opts.why);
|
||||
const madeBy = escapeHtml(opts.madeBy);
|
||||
const privacyUrl = escapeHtml(opts.privacyUrl);
|
||||
const aboutUrl = escapeHtml(opts.aboutUrl);
|
||||
const logoUrl = escapeHtml(opts.logoUrl);
|
||||
const cardBody = description
|
||||
? `<p style="margin:16px 0 0;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:18px;font-weight:500;color:#000000;">${description}</p>`
|
||||
: "";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>${heading}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#e4e4e4;">
|
||||
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">${escapeHtml(
|
||||
PREVIEW_TEXT,
|
||||
)}</div>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#e4e4e4;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px;max-width:600px;background:#ffffff;">
|
||||
<tr>
|
||||
<td style="background:#000000;padding:24px 32px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="font-family:'Bricolage Grotesque',Arial,sans-serif;font-size:22px;line-height:27px;color:#ffffff;padding-right:6px;">CommunityRule</td>
|
||||
<td>
|
||||
<img src="${logoUrl}" width="27" height="27" alt="" style="display:block;border:0;width:27px;height:27px;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:40px 32px 56px;">
|
||||
<h1 style="margin:0 0 16px;font-family:'Bricolage Grotesque',Arial,sans-serif;font-size:36px;line-height:44px;font-weight:700;color:#000000;">${heading}</h1>
|
||||
<p style="margin:0 0 32px;font-family:Inter,Arial,sans-serif;font-size:18px;line-height:24px;color:#303030;">${intro}</p>
|
||||
<p style="margin:0 0 16px;">
|
||||
<a href="${verifyHref}" style="display:inline-block;background:#141414;color:#ffffff;font-family:Inter,Arial,sans-serif;font-size:16px;line-height:20px;font-weight:500;text-decoration:none;border-radius:9999px;padding:12px 16px;">Read the Rule</a>
|
||||
</p>
|
||||
<p style="margin:0 0 4px;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:20px;color:#464646;">Or paste this link into a browser</p>
|
||||
<p style="margin:0 0 32px;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:20px;">
|
||||
<a href="${verifyHref}" style="color:#000000;text-decoration:underline;">${verifyHref}</a>
|
||||
</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#c9fef9;border-radius:12px;">
|
||||
<tr>
|
||||
<td style="padding:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-bottom:1px solid #000000;">
|
||||
<tr>
|
||||
<td width="74" valign="middle" style="padding:8px 8px 8px 4px;">
|
||||
<div style="width:56px;height:56px;border-radius:9999px;background:#fffdd2;font-family:Inter,Arial,sans-serif;font-size:24px;line-height:56px;font-weight:500;color:#000000;text-align:center;">${initials}</div>
|
||||
</td>
|
||||
<td valign="middle" style="border-left:1px solid #000000;padding:12px 16px;font-family:'Bricolage Grotesque',Arial,sans-serif;font-size:24px;line-height:32px;font-weight:700;color:#000000;">${name}</td>
|
||||
</tr>
|
||||
</table>
|
||||
${cardBody}
|
||||
<p style="margin:16px 0 0;">
|
||||
<span style="display:inline-block;background:#303030;color:#c9fef9;font-family:Inter,Arial,sans-serif;font-size:10px;line-height:12px;font-weight:500;letter-spacing:0.2px;text-transform:uppercase;border-radius:2px;padding:2px 4px;">${counts}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="height:1px;background:#e4e4e4;margin:32px 0;"></div>
|
||||
<p style="margin:0 0 20px;font-family:Inter,Arial,sans-serif;font-size:16px;line-height:24px;color:#303030;">${privacyHtml}</p>
|
||||
<p style="margin:0 0 20px;font-family:Inter,Arial,sans-serif;font-size:16px;line-height:24px;color:#303030;">${ownLinkHtml}</p>
|
||||
<p style="margin:0 0 32px;font-family:Inter,Arial,sans-serif;font-size:16px;line-height:24px;color:#303030;">${copyHtml}</p>
|
||||
<div style="border-top:1px solid #e4e4e4;padding-top:24px;">
|
||||
<p style="margin:0 0 12px;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:20px;color:#464646;">${why}</p>
|
||||
<p style="margin:0 0 12px;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:20px;color:#464646;">${madeBy}</p>
|
||||
<p style="margin:0;font-family:Inter,Arial,sans-serif;font-size:14px;line-height:20px;color:#464646;">
|
||||
<a href="${privacyUrl}" style="color:#464646;text-decoration:underline;">Privacy</a>
|
||||
|
||||
<a href="${aboutUrl}" style="color:#464646;text-decoration:underline;">About CommunityRule</a>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function privacyHtmlWithLead(privacy: string): string {
|
||||
const lead = "Every Rule starts private.";
|
||||
if (privacy.startsWith(lead)) {
|
||||
return `<strong style="color:#000000;">${escapeHtml(lead)}</strong>${escapeHtml(
|
||||
privacy.slice(lead.length),
|
||||
)}`;
|
||||
}
|
||||
return escapeHtml(privacy);
|
||||
}
|
||||
|
||||
function leadInHtml(lead: string, full: string): string {
|
||||
if (full.startsWith(lead)) {
|
||||
return `<strong style="color:#000000;">${escapeHtml(lead)}</strong>${escapeHtml(
|
||||
full.slice(lead.length),
|
||||
)}`;
|
||||
}
|
||||
return escapeHtml(full);
|
||||
}
|
||||
Reference in new issue
Block a user