💄 Optimized landing email

This commit is contained in:
LittleSheep 2025-05-17 17:36:34 +08:00
parent d3b56b741e
commit 6728bd5607
8 changed files with 132 additions and 169 deletions

View File

@ -62,19 +62,45 @@ public class EmailService
await client.DisconnectAsync(true); await client.DisconnectAsync(true);
} }
private static string _ConvertHtmlToPlainText(string html)
{
// Remove style tags and their contents
html = System.Text.RegularExpressions.Regex.Replace(html, "<style[^>]*>.*?</style>", "",
System.Text.RegularExpressions.RegexOptions.Singleline);
// Replace header tags with text + newlines
html = System.Text.RegularExpressions.Regex.Replace(html, "<h[1-6][^>]*>(.*?)</h[1-6]>", "$1\n\n",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Replace line breaks
html = html.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n");
// Remove all remaining HTML tags
html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "");
// Decode HTML entities
html = System.Net.WebUtility.HtmlDecode(html);
// Remove excess whitespace
html = System.Text.RegularExpressions.Regex.Replace(html, @"\s+", " ").Trim();
return html;
}
public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail, public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail,
string subject, TModel model, string fallbackTextBody) string subject, TModel model)
where TComponent : IComponent where TComponent : IComponent
{ {
try try
{ {
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model); var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody);
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody); await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
} }
catch (Exception err) catch (Exception err)
{ {
_logger.LogError(err, "Failed to render email template..."); _logger.LogError(err, "Failed to render email template...");
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody); throw;
} }
} }
} }

View File

@ -17,23 +17,21 @@ public class RazorViewRenderer(
ILogger<RazorViewRenderer> logger ILogger<RazorViewRenderer> logger
) )
{ {
public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel model) public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel? model)
where TComponent : IComponent where TComponent : IComponent
{ {
await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory); await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory);
var viewDictionary = new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(),
new ModelStateDictionary())
{
Model = model
};
return await htmlRenderer.Dispatcher.InvokeAsync(async () => return await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{ {
try try
{ {
var parameterView = ParameterView.FromDictionary(viewDictionary); var dictionary = model?.GetType().GetProperties()
.ToDictionary(
prop => prop.Name,
prop => prop.GetValue(model, null)
) ?? new Dictionary<string, object?>();
var parameterView = ParameterView.FromDictionary(dictionary);
var output = await htmlRenderer.RenderComponentAsync<TComponent>(parameterView); var output = await htmlRenderer.RenderComponentAsync<TComponent>(parameterView);
return output.ToHtmlString(); return output.ToHtmlString();
} }

View File

@ -44,9 +44,9 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (contact is null) throw new ArgumentException("Account has no contact method that can use"); if (contact is null) throw new ArgumentException("Account has no contact method that can use");
var link = $"${configuration.GetValue<string>("BaseUrl")}/spells/{Uri.EscapeDataString(spell.Spell)}"; var link = $"{configuration.GetValue<string>("BaseUrl")}/spells/{Uri.EscapeDataString(spell.Spell)}";
logger.LogInformation($"Sending magic spell... {link}"); logger.LogInformation("Sending magic spell... {Link}", link);
try try
{ {
@ -61,8 +61,7 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
{ {
Name = contact.Account.Name, Name = contact.Account.Name,
VerificationLink = link VerificationLink = link
}, }
$"Thank you for creating an account.\nFor accessing all the features, confirm your registration with the link below:\n\n{link}"
); );
break; break;
default: default:

View File

@ -1,18 +1,29 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<div class="min-h-screen bg-gray-100 py-8 font-sans"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<div class="max-w-2xl mx-auto px-8"> <html xmlns="https://www.w3.org/1999/xhtml">
<div class="bg-white rounded-lg shadow-md p-6"> <head>
@ChildContent <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</div> <meta name="viewport" content="width=device-width"/>
<link rel="stylesheet" href="https://unpkg.com/foundation-emails@2.4.0/dist/foundation-emails.min.css">
<style type="text/css">
.container {
padding: 2rem 1rem;
}
</style>
</head>
<div class="text-center mt-6 text-sm text-gray-500"> <body>
<p class="m-0">&copy; @DateTime.Now.Year DysonNetwork. All rights reserved.</p> <table class="body">
</div> <tr>
</div> <td class="float-center" align="center" valign="top" style="padding: 2rem 1rem;">
</div> @ChildContent
</td>
</tr>
</table>
</body>
</html>
@code { @code {
[Parameter] [Parameter] public RenderFragment? ChildContent { get; set; }
public RenderFragment? ChildContent { get; set; }
} }

View File

@ -1,44 +1,56 @@
<EmailLayout> <EmailLayout>
<div style="text-align: center; margin-bottom: 2rem;"> <table class="container">
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0;">Welcome to DysonNetwork!</h1> <tr>
</div> <td class="columns">
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0; text-align: center;">
Welcome to the Solar Network!
</h1>
</td>
</tr>
<div style="display: flex; flex-direction: column; gap: 1.5rem;"> <tr>
<td class="columns">
<p style="color: #374151; margin: 0;"> <p style="color: #374151; margin: 0;">
Dear @Name, Dear @Name,
</p> </p>
<p style="color: #374151; margin: 0;"> <p style="color: #374151; margin: 0;">
Thank you for creating an account with DysonNetwork. We're excited to have you join our community! Thank you for creating an account on the Solar Network. We're excited to have you join our community!
</p> </p>
<p style="color: #374151; margin: 0;"> <p style="color: #374151; margin: 0;">
To access all features and ensure the security of your account, please confirm your registration by clicking To access all features and ensure the security of your account, please confirm your registration by
the button below: clicking the button below:
</p> </p>
</td>
</tr>
<div style="text-align: center; margin: 2rem 0;"> <tr>
<a href="@VerificationLink" <td class="columns">
style="display: inline-block; padding: 0.75rem 1.5rem; background-color: #2563eb; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 0.5rem;"> <div style="text-align: center;">
<button href="@VerificationLink"
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
Confirm Registration Confirm Registration
</a> </button>
</div> </div>
</td>
</tr>
<tr>
<td class="columns">
<p style="color: #374151; margin: 0;"> <p style="color: #374151; margin: 0;">
If the button doesn't work, you can also copy and paste this link into your browser: If the button doesn't work, you can also copy and paste this link into your browser:
<br> <br>
<span style="color: #2563eb; word-break: break-all;">@VerificationLink</span> <a href="@VerificationLink" style="color: #2563eb; word-break: break-all;">@VerificationLink</a>
</p> </p>
<p style="color: #374151; margin: 0;"> <p style="color: #374151; margin: 0;">
If you didn't create this account, please ignore this email. If you didn't create this account, please ignore this email.
</p> </p>
<p style="color: #374151; margin: 2rem 0 0 0;"> <p style="color: #374151; margin: 2rem 0 0 0;">
Best regards,<br> Best regards,<br>
The DysonNetwork Team The Solar Network Team
</p> </p>
</div> </td>
</tr>
</table>
</EmailLayout> </EmailLayout>
@code { @code {

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewData["Title"]</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f3f4f6; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 42rem; margin: 0 auto; padding: 2rem;">
<div style="background-color: #ffffff; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); padding: 1.5rem;">
@RenderBody()
</div>
<div style="text-align: center; margin-top: 1.5rem; font-size: 0.875rem; color: #6b7280;">
<p style="margin: 0;">&copy; @DateTime.Now.Year DysonNetwork. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@ -1,6 +1,6 @@
{ {
"Debug": true, "Debug": true,
"BaseUrl": "http://localhost:5017", "BaseUrl": "http://localhost:5071",
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",

View File

@ -227,15 +227,27 @@
.z-50 { .z-50 {
z-index: 50; z-index: 50;
} }
.m-0 { .container {
margin: calc(var(--spacing) * 0); width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
} }
.mx-auto { .mx-auto {
margin-inline: auto; margin-inline: auto;
} }
.my-8 {
margin-block: calc(var(--spacing) * 8);
}
.mt-4 { .mt-4 {
margin-top: calc(var(--spacing) * 4); margin-top: calc(var(--spacing) * 4);
} }
@ -269,15 +281,15 @@
.block { .block {
display: block; display: block;
} }
.contents {
display: contents;
}
.flex { .flex {
display: flex; display: flex;
} }
.hidden { .hidden {
display: none; display: none;
} }
.inline-block {
display: inline-block;
}
.table { .table {
display: table; display: table;
} }
@ -290,9 +302,6 @@
.h-full { .h-full {
height: 100%; height: 100%;
} }
.min-h-screen {
min-height: 100vh;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -317,13 +326,6 @@
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
.space-y-6 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
}
}
.gap-x-6 { .gap-x-6 {
column-gap: calc(var(--spacing) * 6); column-gap: calc(var(--spacing) * 6);
} }
@ -333,12 +335,6 @@
.bg-blue-500 { .bg-blue-500 {
background-color: var(--color-blue-500); background-color: var(--color-blue-500);
} }
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-green-100 { .bg-green-100 {
background-color: var(--color-green-100); background-color: var(--color-green-100);
} }
@ -354,33 +350,21 @@
.p-6 { .p-6 {
padding: calc(var(--spacing) * 6); padding: calc(var(--spacing) * 6);
} }
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.px-6 { .px-6 {
padding-inline: calc(var(--spacing) * 6); padding-inline: calc(var(--spacing) * 6);
} }
.px-8 {
padding-inline: calc(var(--spacing) * 8);
}
.py-3 { .py-3 {
padding-block: calc(var(--spacing) * 3); padding-block: calc(var(--spacing) * 3);
} }
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
.py-12 { .py-12 {
padding-block: calc(var(--spacing) * 12); padding-block: calc(var(--spacing) * 12);
} }
.text-center { .text-center {
text-align: center; text-align: center;
} }
.font-sans {
font-family: var(--font-sans);
}
.text-2xl { .text-2xl {
font-size: var(--text-2xl); font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height)); line-height: var(--tw-leading, var(--text-2xl--line-height));
@ -405,10 +389,6 @@
font-size: var(--text-xl); font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height)); line-height: var(--tw-leading, var(--text-xl--line-height));
} }
.leading-6 {
--tw-leading: calc(var(--spacing) * 6);
line-height: calc(var(--spacing) * 6);
}
.leading-8 { .leading-8 {
--tw-leading: calc(var(--spacing) * 8); --tw-leading: calc(var(--spacing) * 8);
line-height: calc(var(--spacing) * 8); line-height: calc(var(--spacing) * 8);
@ -429,12 +409,6 @@
--tw-tracking: var(--tracking-tight); --tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight); letter-spacing: var(--tracking-tight);
} }
.break-all {
word-break: break-all;
}
.text-blue-600 {
color: var(--color-blue-600);
}
.text-gray-500 { .text-gray-500 {
color: var(--color-gray-500); color: var(--color-gray-500);
} }
@ -467,10 +441,6 @@
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-md {
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-sm { .shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@ -504,13 +474,6 @@
} }
} }
} }
.hover\:bg-blue-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-700);
}
}
}
.hover\:text-blue-600 { .hover\:text-blue-600 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@ -518,13 +481,6 @@
} }
} }
} }
.hover\:text-gray-700 {
&:hover {
@media (hover: hover) {
color: var(--color-gray-700);
}
}
}
.focus\:ring-2 { .focus\:ring-2 {
&:focus { &:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
@ -568,11 +524,6 @@
background-color: var(--color-yellow-900); background-color: var(--color-yellow-900);
} }
} }
.dark\:text-gray-100 {
@media (prefers-color-scheme: dark) {
color: var(--color-gray-100);
}
}
.dark\:text-gray-300 { .dark\:text-gray-300 {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color: var(--color-gray-300); color: var(--color-gray-300);
@ -607,15 +558,6 @@
} }
} }
} }
.dark\:hover\:text-gray-300 {
@media (prefers-color-scheme: dark) {
&:hover {
@media (hover: hover) {
color: var(--color-gray-300);
}
}
}
}
} }
@layer theme, base, components, utilities; @layer theme, base, components, utilities;
@layer theme; @layer theme;
@ -858,11 +800,6 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-leading { @property --tw-leading {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@ -1016,7 +953,6 @@
--tw-rotate-z: initial; --tw-rotate-z: initial;
--tw-skew-x: initial; --tw-skew-x: initial;
--tw-skew-y: initial; --tw-skew-y: initial;
--tw-space-y-reverse: 0;
--tw-leading: initial; --tw-leading: initial;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-tracking: initial; --tw-tracking: initial;