Кликабельные ссылки: история фидбека у куратора и цитаты в письмах
Куратор видел адрес в своём фидбеке мёртвым текстом — linkify там не было. В письмах (фидбек, ответы на вопросы и комментарии) quote() только экранировал: кликабельность зависела от почтовика. Теперь ссылки оформлены <a>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -229,7 +229,7 @@ export default async function SubmissionPage({ params }: Props) {
|
||||
{new Date(fb.createdAt).toLocaleString("ru-RU")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm whitespace-pre-wrap mb-2">{fb.text}</p>
|
||||
<p className="text-sm whitespace-pre-wrap mb-2">{linkify(fb.text)}</p>
|
||||
{fbFiles.length > 0 && (
|
||||
<div className="space-y-1 mt-2">
|
||||
{fbFiles.map((f) => (
|
||||
|
||||
@@ -83,4 +83,20 @@ describe("sendCommentReplyEmail", () => {
|
||||
expect(html).not.toContain("Отвечает <strong>");
|
||||
expect(html).toContain("Привет!");
|
||||
});
|
||||
|
||||
it("делает ссылки в тексте ответа кликабельными", async () => {
|
||||
await sendCommentReplyEmail(
|
||||
"student@example.com",
|
||||
"Екатерина",
|
||||
"6.3. Холст",
|
||||
"Подарок тут: https://obsidian.second-brain.ru/secret-bonus?a=1&b=2. Пишите!",
|
||||
LESSON_URL
|
||||
);
|
||||
|
||||
const html = sent[0].html;
|
||||
expect(html).toContain(
|
||||
'<a href="https://obsidian.second-brain.ru/secret-bonus?a=1&b=2" target="_blank"'
|
||||
);
|
||||
expect(html).toContain("secret-bonus?a=1&b=2</a>. Пишите!");
|
||||
});
|
||||
});
|
||||
|
||||
+15
-1
@@ -1,5 +1,6 @@
|
||||
import { Resend } from "resend";
|
||||
import { getSetting } from "./settings";
|
||||
import { URL_RE } from "./linkify";
|
||||
|
||||
function getResend() {
|
||||
return new Resend(process.env.RESEND_API_KEY);
|
||||
@@ -97,11 +98,24 @@ function escapeHtml(text: string) {
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// Ссылки в цитате — кликабельные: не все почтовики сами превращают голый адрес в ссылку.
|
||||
// Экранируем по частям, чтобы `&` в адресе не превратился в `&` внутри href дважды.
|
||||
function linkifyHtml(text: string) {
|
||||
return text
|
||||
.split(URL_RE)
|
||||
.map((part, i) =>
|
||||
i % 2 === 1
|
||||
? `<a href="${escapeHtml(part)}" target="_blank" style="color:#323232;text-decoration:underline;word-break:break-all;">${escapeHtml(part)}</a>`
|
||||
: escapeHtml(part)
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
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;">${escapeHtml(text).replace(/\n/g, "<br/>")}</p>
|
||||
<p style="margin:0;font-family:'Courier New',Courier,monospace;font-size:13px;line-height:1.6;color:#323232;">${linkifyHtml(text).replace(/\n/g, "<br/>")}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Fragment, type ReactNode } from "react";
|
||||
|
||||
const URL_RE = /(https?:\/\/[^\s<>()]+[^\s<>().,;:!?])/g;
|
||||
export const URL_RE = /(https?:\/\/[^\s<>()]+[^\s<>().,;:!?])/g;
|
||||
|
||||
export function linkify(text: string): ReactNode[] {
|
||||
if (!text) return [];
|
||||
|
||||
Reference in New Issue
Block a user