💄 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);
}
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,
string subject, TModel model, string fallbackTextBody)
string subject, TModel model)
where TComponent : IComponent
{
try
{
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody);
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
}
catch (Exception err)
{
_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
)
{
public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel model)
public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel? model)
where TComponent : IComponent
{
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 () =>
{
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);
return output.ToHtmlString();
}

View File

@ -44,9 +44,9 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
.FirstOrDefaultAsync();
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
{
@ -61,8 +61,7 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
{
Name = contact.Account.Name,
VerificationLink = link
},
$"Thank you for creating an account.\nFor accessing all the features, confirm your registration with the link below:\n\n{link}"
}
);
break;
default:

View File

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

View File

@ -1,44 +1,56 @@
<EmailLayout>
<div style="text-align: center; margin-bottom: 2rem;">
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0;">Welcome to DysonNetwork!</h1>
</div>
<table class="container">
<tr>
<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;">
Dear @Name,
</p>
<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 style="color: #374151; margin: 0;">
To access all features and ensure the security of your account, please confirm your registration by clicking
the button below:
To access all features and ensure the security of your account, please confirm your registration by
clicking the button below:
</p>
</td>
</tr>
<div style="text-align: center; margin: 2rem 0;">
<a href="@VerificationLink"
style="display: inline-block; padding: 0.75rem 1.5rem; background-color: #2563eb; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 0.5rem;">
<tr>
<td class="columns">
<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
</a>
</button>
</div>
</td>
</tr>
<tr>
<td class="columns">
<p style="color: #374151; margin: 0;">
If the button doesn't work, you can also copy and paste this link into your browser:
<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 style="color: #374151; margin: 0;">
If you didn't create this account, please ignore this email.
</p>
<p style="color: #374151; margin: 2rem 0 0 0;">
Best regards,<br>
The DysonNetwork Team
The Solar Network Team
</p>
</div>
</td>
</tr>
</table>
</EmailLayout>
@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,
"BaseUrl": "http://localhost:5017",
"BaseUrl": "http://localhost:5071",
"Logging": {
"LogLevel": {
"Default": "Information",

View File

@ -227,15 +227,27 @@
.z-50 {
z-index: 50;
}
.m-0 {
margin: calc(var(--spacing) * 0);
.container {
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 {
margin-inline: auto;
}
.my-8 {
margin-block: calc(var(--spacing) * 8);
}
.mt-4 {
margin-top: calc(var(--spacing) * 4);
}
@ -269,15 +281,15 @@
.block {
display: block;
}
.contents {
display: contents;
}
.flex {
display: flex;
}
.hidden {
display: none;
}
.inline-block {
display: inline-block;
}
.table {
display: table;
}
@ -290,9 +302,6 @@
.h-full {
height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
.w-full {
width: 100%;
}
@ -317,13 +326,6 @@
.justify-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 {
column-gap: calc(var(--spacing) * 6);
}
@ -333,12 +335,6 @@
.bg-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 {
background-color: var(--color-green-100);
}
@ -354,33 +350,21 @@
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.px-8 {
padding-inline: calc(var(--spacing) * 8);
}
.py-3 {
padding-block: calc(var(--spacing) * 3);
}
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
.py-12 {
padding-block: calc(var(--spacing) * 12);
}
.text-center {
text-align: center;
}
.font-sans {
font-family: var(--font-sans);
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@ -405,10 +389,6 @@
font-size: var(--text-xl);
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 {
--tw-leading: calc(var(--spacing) * 8);
line-height: calc(var(--spacing) * 8);
@ -429,12 +409,6 @@
--tw-tracking: 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 {
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));
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 {
--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);
@ -504,13 +474,6 @@
}
}
}
.hover\:bg-blue-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-700);
}
}
}
.hover\:text-blue-600 {
&: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 {
--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);
}
}
.dark\:text-gray-100 {
@media (prefers-color-scheme: dark) {
color: var(--color-gray-100);
}
}
.dark\:text-gray-300 {
@media (prefers-color-scheme: dark) {
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;
@ -858,11 +800,6 @@
syntax: "*";
inherits: false;
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-leading {
syntax: "*";
inherits: false;
@ -1016,7 +953,6 @@
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-space-y-reverse: 0;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-tracking: initial;