Skip to content

Commit 92a03ac

Browse files
committed
feat(broadcast): add markdown preview and render markdown in emails
Add a write/preview tab toggle to the broadcast message input, mirroring the pattern used on the event description field. The Go backend now converts the markdown body to HTML via blackfriday before injecting it into the email template, replacing the plain-text white-space:pre-line approach.
1 parent 7e23235 commit 92a03ac

6 files changed

Lines changed: 55 additions & 16 deletions

File tree

app/(dashboard)/dashboard/event/[id]/(default)/broadcast/dashboard-broadcast-form.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import { FormFieldCheckbox } from "@/components/widgets/form/form-field-checkbox
1919
import { ButtonWithChildren } from "@/components/widgets/buttons/button-with-children";
2020
import { Form } from "@/components/shadcn/form";
2121
import { captureException } from "@/lib/report";
22+
import { Tabs, TabsContent } from "@/components/shadcn/tabs";
23+
import TabsIconsList from "@/components/widgets/tabs/tabs-icons-list";
24+
import { getMarkdownEditorTabs } from "@/lib/markdown-editor";
25+
import { MarkdownPreview } from "@/components/widgets/markdown-preview";
2226

2327
interface DashboardBroadcastFormProps {
2428
eventId: string;
@@ -37,19 +41,44 @@ function BroadcastEmailForm({
3741
isDisabled: boolean;
3842
} & React.ComponentProps<"form">) {
3943
const t = useTranslations("broadcast-email-form");
44+
const message = form.watch("message");
4045

4146
return (
4247
<form onSubmit={onSubmit} className={cn("flex flex-col gap-2", className)}>
43-
<FormFieldTextArea
44-
control={form.control}
45-
name="message"
46-
placeholder={t("message-input-placeholder")}
47-
label={t("message-input-label")}
48-
className="min-h-[100px] max-h-[500px]"
49-
maxLength={5000}
50-
wordCounter
51-
disabled={isDisabled}
52-
/>
48+
<Tabs defaultValue="write" className="w-full">
49+
<div className="flex flex-row items-center justify-between mb-1">
50+
<span className="text-sm font-medium">
51+
{t("message-input-label")}
52+
</span>
53+
<TabsIconsList
54+
tabs={getMarkdownEditorTabs({
55+
writeLabel: t("write-tab"),
56+
previewLabel: t("preview-tab"),
57+
})}
58+
className="rounded p-0 h-fit"
59+
/>
60+
</div>
61+
<TabsContent value="write" tabIndex={-1}>
62+
<FormFieldTextArea
63+
control={form.control}
64+
name="message"
65+
placeholder={t("message-input-placeholder")}
66+
className="min-h-[100px] max-h-[500px]"
67+
maxLength={5000}
68+
wordCounter
69+
disabled={isDisabled}
70+
/>
71+
</TabsContent>
72+
<TabsContent value="preview">
73+
{message.trim() === "" ? (
74+
<div className="w-full h-32 flex items-center justify-center text-sm text-muted-foreground">
75+
{t("preview-empty")}
76+
</div>
77+
) : (
78+
<MarkdownPreview markdownString={message} />
79+
)}
80+
</TabsContent>
81+
</Tabs>
5382
<div className="mb-4">
5483
<FormFieldCheckbox
5584
control={form.control}

app/i18n/messages/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,10 @@
640640
"attach-ticket-label": "Attach ticket to the message",
641641
"toast-email-sent-success": "Email sent !",
642642
"toast-email-sent-error": "Error while trying to send the email",
643-
"no-participant-error": "You can't send any message because nobody is going to your event yet"
643+
"no-participant-error": "You can't send any message because nobody is going to your event yet",
644+
"write-tab": "Write",
645+
"preview-tab": "Preview",
646+
"preview-empty": "Nothing to preview"
644647
},
645648
"event-community-section": {
646649
"organized-by": "Organized by community"

app/i18n/messages/es.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,10 @@
639639
"attach-ticket-label": "Adjuntar entrada al mensaje",
640640
"toast-email-sent-success": "¡Email enviado!",
641641
"toast-email-sent-error": "Error al intentar enviar el email",
642-
"no-participant-error": "No puedes enviar ningún mensaje porque nadie asiste a tu evento todavía"
642+
"no-participant-error": "No puedes enviar ningún mensaje porque nadie asiste a tu evento todavía",
643+
"write-tab": "Escribir",
644+
"preview-tab": "Vista previa",
645+
"preview-empty": "Nada que mostrar"
643646
},
644647
"event-community-section": {
645648
"organized-by": "Organizado por la comunidad"

app/i18n/messages/fr.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,10 @@
640640
"attach-ticket-label": "Joindre le billet au message",
641641
"toast-email-sent-success": "Email envoyé !",
642642
"toast-email-sent-error": "Erreur lors de l'envoi de l'email",
643-
"no-participant-error": "Vous ne pouvez envoyer aucun message car personne ne participe encore à votre événement"
643+
"no-participant-error": "Vous ne pouvez envoyer aucun message car personne ne participe encore à votre événement",
644+
"write-tab": "Écrire",
645+
"preview-tab": "Aperçu",
646+
"preview-empty": "Rien à afficher"
644647
},
645648
"event-community-section": {
646649
"organized-by": "Organisé par la communauté"

backend/mail_templates.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
texttemplate "text/template"
99
"time"
1010

11+
"github.com/russross/blackfriday/v2"
1112
"github.com/samouraiworld/zenao/backend/zeni"
1213
)
1314

@@ -232,15 +233,15 @@ func formatAmountMinor(amountMinor int64, currencyCode string) string {
232233
type eventBroadcast struct {
233234
EventName string
234235
ImageURL string
235-
Message string
236+
Message template.HTML
236237
EventURL string
237238
}
238239

239240
func eventBroadcastMailContent(event *zeni.Event, message string) (string, string, error) {
240241
data := eventBroadcast{
241242
ImageURL: web2URL(event.ImageURI) + "?img-width=960&img-height=540&img-fit=cover&dpr=2",
242243
EventName: event.Title,
243-
Message: message,
244+
Message: template.HTML(blackfriday.Run([]byte(message))),
244245
EventURL: eventPublicURL(event.ID),
245246
}
246247

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.ImageURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="background-color:#ffffff"><!--$--><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="background-color:#ffffff;color:#000000;font-family:&quot;Helvetica Neue&quot;,-apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" data-skip-in-text="true">Message from organizer of {{.EventName}}<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:800px;margin:10px auto;border:1px solid #F5F5F5"><tbody><tr style="width:100%"><td><img alt="Event image" src="{{.ImageURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:100%;object-fit:cover;aspect-ratio:16/9"/><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:48px 20px;height:220px;background-color:#000000;word-break:break-word"><tbody><tr><td><p style="font-size:48px;line-height:1.1;color:#FFFFFF;text-align:center;font-weight:500;margin:0;letter-spacing:-1.2px;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Message from <!-- -->{{.EventName}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:48px 20px"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="background-color:#F5F5F5;border-radius:8px;padding:20px 20px 20px 20px;margin-bottom:24px;border-left:4px solid #000000"><tbody><tr><td><p style="font-size:16px;line-height:1.6;margin:0;color:#333333;white-space:pre-line;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Message}}</p></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><a href="{{.EventURL}}" style="line-height:1.3;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#FFFFFF;font-size:16px;width:100%;border-radius:4px;margin-top:16px;text-align:center;padding-top:14px;padding-bottom:14px;font-weight:500" target="_blank"><span><!--[if mso]><i style="mso-font-width:0%;mso-text-raise:21" hidden></i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:10.5px">View event details</span><span><!--[if mso]><i style="mso-font-width:0%" hidden>&#8203;</i><![endif]--></span></a></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:20px;background-color:#F5F5F5;border-bottom-left-radius:4px;border-bottom-right-radius:4px"><tbody><tr><td><p style="font-size:12px;line-height:24px;color:#666666;text-align:center;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">You&#x27;re receiving this email because you&#x27;re a participant of<!-- --> <!-- -->{{.EventName}}<!-- -->.</p></td></tr></tbody></table></td></tr></tbody></table></td></tr></tbody></table><!--7--><!--/$--></body></html>
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.ImageURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="background-color:#ffffff"><!--$--><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="background-color:#ffffff;color:#000000;font-family:&quot;Helvetica Neue&quot;,-apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Oxygen-Sans,Ubuntu,Cantarell,sans-serif"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" data-skip-in-text="true">Message from organizer of {{.EventName}}<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:800px;margin:10px auto;border:1px solid #F5F5F5"><tbody><tr style="width:100%"><td><img alt="Event image" src="{{.ImageURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:100%;object-fit:cover;aspect-ratio:16/9"/><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:48px 20px;height:220px;background-color:#000000;word-break:break-word"><tbody><tr><td><p style="font-size:48px;line-height:1.1;color:#FFFFFF;text-align:center;font-weight:500;margin:0;letter-spacing:-1.2px;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Message from <!-- -->{{.EventName}}</p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:48px 20px"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="background-color:#F5F5F5;border-radius:8px;padding:20px 20px 20px 20px;margin-bottom:24px;border-left:4px solid #000000"><tbody><tr><td><div style="font-size:16px;line-height:1.6;margin:0;color:#333333;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.Message}}</div></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><a href="{{.EventURL}}" style="line-height:1.3;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:#000000;color:#FFFFFF;font-size:16px;width:100%;border-radius:4px;margin-top:16px;text-align:center;padding-top:14px;padding-bottom:14px;font-weight:500" target="_blank"><span><!--[if mso]><i style="mso-font-width:0%;mso-text-raise:21" hidden></i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:10.5px">View event details</span><span><!--[if mso]><i style="mso-font-width:0%" hidden>&#8203;</i><![endif]--></span></a></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:20px;background-color:#F5F5F5;border-bottom-left-radius:4px;border-bottom-right-radius:4px"><tbody><tr><td><p style="font-size:12px;line-height:24px;color:#666666;text-align:center;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">You&#x27;re receiving this email because you&#x27;re a participant of<!-- --> <!-- -->{{.EventName}}<!-- -->.</p></td></tr></tbody></table></td></tr></tbody></table></td></tr></tbody></table><!--7--><!--/$--></body></html>

0 commit comments

Comments
 (0)