Polish answer notifications and editing controls.
CI / test (pull_request) Successful in 6m24s

Brand notification emails, log accepted sends, and keep answer editing collapsed until explicitly opened.
This commit is contained in:
2026-08-26 23:39:47 -07:00
parent f61b4fc7ab
commit ff111d619d
8 changed files with 258 additions and 23 deletions
+101 -20
View File
@@ -2,6 +2,7 @@ package mail
import (
"context"
_ "embed"
"fmt"
"html"
"os"
@@ -10,6 +11,9 @@ import (
"github.com/resend/resend-go/v3"
)
//go:embed mark.png
var markPNG []byte
// QuestionAnswered is the payload for notifying a question author of a reply.
type QuestionAnswered struct {
ToEmail string
@@ -66,7 +70,29 @@ func (r *Resend) NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswere
if to == "" {
return nil
}
link := r.baseURL + "/questions/" + msg.QuestionID
text, htmlBody := questionAnsweredContent(r.baseURL, msg)
params := &resend.SendEmailRequest{
From: r.from,
To: []string{to},
Subject: "Your question was answered",
Text: text,
Html: htmlBody,
Attachments: []*resend.Attachment{{
Content: markPNG,
Filename: "ask-a-plumber-first.png",
ContentType: "image/png",
ContentId: "answer-notification-mark",
}},
}
opts := &resend.SendEmailOptions{
IdempotencyKey: "answer-notify:" + msg.QuestionID,
}
_, err := r.client.Emails.SendWithOptions(ctx, params, opts)
return err
}
func questionAnsweredContent(baseURL string, msg QuestionAnswered) (string, string) {
link := strings.TrimRight(baseURL, "/") + "/questions/" + msg.QuestionID
title := strings.TrimSpace(msg.QuestionTitle)
if title == "" {
title = "your question"
@@ -78,27 +104,82 @@ func (r *Resend) NotifyQuestionAnswered(ctx context.Context, msg QuestionAnswere
msg.AnswerBody,
link,
)
htmlBody := fmt.Sprintf(
`<p>Hi%s,</p><p>Your question <strong>%s</strong> has an answer from a plumber:</p><blockquote style="margin:1em 0;padding:0.75em 1em;border-left:3px solid #ccc;white-space:pre-wrap">%s</blockquote><p><a href="%s">View the answer</a></p>`,
html.EscapeString(greetingName(msg.ToName)),
html.EscapeString(title),
html.EscapeString(msg.AnswerBody),
html.EscapeString(link),
)
params := &resend.SendEmailRequest{
From: r.from,
To: []string{to},
Subject: "Your question was answered",
Text: text,
Html: htmlBody,
}
opts := &resend.SendEmailOptions{
IdempotencyKey: "answer-notify:" + msg.QuestionID,
}
_, err := r.client.Emails.SendWithOptions(ctx, params, opts)
return err
htmlBody := strings.NewReplacer(
"{{PREHEADER}}", html.EscapeString("A plumber answered "+title+"."),
"{{GREETING}}", html.EscapeString(greetingName(msg.ToName)),
"{{TITLE}}", html.EscapeString(title),
"{{ANSWER}}", html.EscapeString(msg.AnswerBody),
"{{LINK}}", html.EscapeString(link),
"{{MARK}}", "cid:answer-notification-mark",
).Replace(questionAnsweredHTML)
return text, htmlBody
}
const questionAnsweredHTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="supported-color-schemes" content="dark">
<title>Your question was answered</title>
</head>
<body style="margin:0;padding:0;background:#161719;color:#ecebe7;font-family:Arial,'Helvetica Neue',sans-serif;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">{{PREHEADER}}</div>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;background:#161719;">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" width="560" cellspacing="0" cellpadding="0" border="0" style="width:100%;max-width:560px;background:#1e2023;border:1px solid #2e3136;border-top:3px solid #e96a26;border-radius:3px;">
<tr>
<td style="padding:24px 28px 20px;border-bottom:1px solid #2e3136;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="padding-right:12px;vertical-align:middle;">
<img src="{{MARK}}" width="32" height="32" alt="" style="display:block;width:32px;height:32px;border:0;">
</td>
<td style="vertical-align:middle;">
<div style="color:#ecebe7;font-size:14px;font-weight:700;line-height:1.2;letter-spacing:1px;text-transform:uppercase;">Ask a Plumber First</div>
<div style="margin-top:4px;color:#8d9096;font-family:'Courier New',monospace;font-size:10px;line-height:1.2;letter-spacing:1.4px;text-transform:uppercase;">Bay Area · Shop Dispatch</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:30px 28px 32px;">
<div style="margin:0 0 10px;color:#e96a26;font-family:'Courier New',monospace;font-size:11px;font-weight:700;line-height:1.4;letter-spacing:1.8px;text-transform:uppercase;">Shop response</div>
<h1 style="margin:0;color:#ecebe7;font-size:28px;font-weight:600;line-height:1.2;letter-spacing:-0.4px;">Your question has an answer.</h1>
<p style="margin:18px 0 0;color:#b8babf;font-size:16px;line-height:1.6;">Hi{{GREETING}}, a plumber replied to:</p>
<p style="margin:8px 0 0;color:#ecebe7;font-size:17px;font-weight:600;line-height:1.45;">“{{TITLE}}”</p>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;margin-top:24px;background:#161719;border:1px solid #2e3136;border-radius:3px;">
<tr>
<td style="padding:20px 18px;">
<div style="margin:0 0 10px;color:#8d9096;font-family:'Courier New',monospace;font-size:10px;font-weight:700;line-height:1.4;letter-spacing:1.5px;text-transform:uppercase;">The answer</div>
<div style="margin:0;color:#ecebe7;font-size:16px;line-height:1.65;white-space:pre-wrap;">{{ANSWER}}</div>
</td>
</tr>
</table>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin-top:26px;">
<tr>
<td bgcolor="#e96a26" style="border-radius:3px;">
<a href="{{LINK}}" style="display:inline-block;padding:13px 18px;color:#161719;font-family:'Courier New',monospace;font-size:12px;font-weight:700;line-height:1;text-decoration:none;letter-spacing:0.8px;text-transform:uppercase;">View the answer&nbsp;&rarr;</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:18px 28px;border-top:1px solid #2e3136;color:#8d9096;font-family:'Courier New',monospace;font-size:10px;line-height:1.6;letter-spacing:0.4px;">
You received this because you asked a question on Ask a Plumber First.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`
func greetingName(name string) string {
name = strings.TrimSpace(name)
if name == "" {