NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Saturday, 21 September 2024
//Less than a minute
Während der Durchsuchung der Blogs anderer Leute bemerkte ich, dass viele von ihnen einen Abonnement-Service haben, der Benutzern erlaubt, sich zu registrieren, um eine E-Mail an sie wöchentlich mit den Beiträgen von diesem Blog gesendet zu haben. Ich beschloss, meine eigene Version davon zu implementieren und zu teilen, wie ich es getan habe.
HINWEIS: Ich erwarte nicht, dass jemand dies tatsächlich benutzen wird, ich habe viel Zeit auf meinen Händen, nachdem ein Vertrag durchgefallen ist, also hält mich das beschäftigt!
Um diesen Service zu schaffen, habe ich mich für folgende Anforderungen entschieden.
Die Auswirkung ist, dass ich viel Arbeit zu tun habe.
Ich begann mit dem lustigen Teil und schrieb eine Abonnement-Seite. Ich wollte, dass dies gut funktioniert auf dem Desktop sowie mobile Browser (und sogar die SPA); von meine Umami-Analytik Ich sehe einen gerechten Anteil der Nutzer von mobilen Geräten aus auf diese Aufregung zugreifen.
Ich wollte auch, dass die Abonnement-Seite offensichtlich sein, wie man verwendet; Ich bin ein großer Gläubiger in Steve Krugs "Zwing mich nicht zum Nachdenken" Philosophie, wo eine Seite sollte offensichtlich sein, um zu verwenden und nicht verlangen, dass der Benutzer darüber nachzudenken, wie man es verwenden.
Dies bedeutet, dass die Voreinstellungen sollte die Mehrheit der Benutzer wollen. Ich habe mich für folgende Ausfälle entschieden:
Ich kann das natürlich später ändern, wenn sich diese als falsch erweisen.
Das ist also die Seite, die ich aufgebaut habe:
Wie beim Rest dieser Seite wollte ich den Code so einfach wie möglich machen. Es verwendet das folgende HTML:
@using Mostlylucid.Shared
@using Mostlylucid.Shared.Helpers
@model Mostlylucid.EmailSubscription.Models.EmailSubscribeViewModel
<form x-data="{schedule :'@Model.SubscriptionType'}" x-init="$watch('schedule', value => console.log(value))" hx-boost="true" asp-action="Save" asp-controller="EmailSubscription"
hx-target="#contentcontainer" hx-swap="#outerHTML">
<div class="flex flex-col mb-4">
<div class="flex flex-wrap lg:flex-nowrap lg:space-x-4 space-y-4 lg:space-y-0 items-start">
<label class="input input-bordered flex items-center gap-2 mb-2 dark:bg-custom-dark-bg bg-white w-full lg:w-2/3">
<i class='bx bx-envelope'></i>
<input type="email" class="grow text-black dark:text-white bg-transparent border-0"
asp-for="Email" placeholder="Email (optional)"/>
</label>
<div class="grid grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))] w-full lg:w-1/3">
@{
var frequency = Enum.GetValues(typeof(SubscriptionType)).Cast<SubscriptionType>().ToList();
}
@foreach (var freq in frequency)
{
<div class="flex items-center w-auto h-full min-h-[30px] lg:mb-0 mb-3">
<input x-model="schedule" id="@freq" type="radio" value="@freq.ToString()" name="SubscriptionType" class="hidden peer">
<label for="@freq" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@freq.EnumDisplayName()
</label>
</div>
}
</div>
</div>
@{
var languages = LanguageConverter.LanguageMap;
}
<div class="grid grid-cols-[repeat(auto-fit,minmax(85px,1fr))] mt-4 gap-2 pl-6 large:pl-0 w-auto">
@foreach (var language in languages)
{
var [email protected] == language.Key ? "checked" : "";
<div class="tooltip lg:mb-0 mb-2" data-tip="@language.Value)" >
<div class="flex items-center justify-center w-[85px] h-full min-h-[70px]">
<input id="@language.Key" type="radio" value="@language.Key" @isChecked name="language" class="hidden peer">
<label for="@language.Key" class="flex flex-col items-center justify-center text-sm font-medium text-white bg-blue-dark opacity-50 peer-checked:opacity-100 w-full h-full">
<img src="/img/flags/@(language.Key).svg" asp-append-version="true" class="border-gray-light border rounded-l w-full h-full object-cover" alt="@language.Value">
</label>
</div>
</div>
}
</div>
<div class="mt-3 border-neutral-400 dark:border-neutral-600 border rounded-lg" x-data="{ hideCategories: false, showCategories: false }">
<h4
class="px-5 py-1 bg-neutral-500 bg-opacity-10 rounded-lg font-body text-primary dark:text-white w-full flex justify-between items-center cursor-pointer"
x-on:click="if(!hideCategories) { showCategories = !showCategories }">
<span class="flex flex-row items-center space-x-1">
Categories
<label class="label cursor-pointer ml-4" x-on:click.stop="">
all
</label>
<input type="checkbox" x-on:click.stop="" x-model="hideCategories" asp-for="AllCategories" class="toggle toggle-info toggle-sm" />
</span>
<span>
<i
class="bx text-2xl"
x-show="!hideCategories"
:class="showCategories ? 'bx-chevron-up' : 'bx-chevron-down'"></i>
</span>
</h4>
<div class="flex flex-wrap gap-2 pt-2 pl-5 pr-5 pb-2"
x-show="showCategories"
x-cloak
x-transition:enter="max-h-0 opacity-0"
x-transition:enter-end="max-h-screen opacity-100"
x-transition:leave="max-h-screen opacity-100"
x-transition:leave-end="max-h-0 opacity-0">
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] mt-4 w-full">
@foreach (var category in Model.Categories)
{
var categoryKey = category.Replace(" ", "_").Replace(".", "_").Replace("-", "_");
<div class="flex items-center w-auto h-full min-h-[50px]">
<input id="@categoryKey" type="checkbox" value="@category" name="@nameof(Model.SelectedCategories)" class="hidden peer">
<label for="@categoryKey" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@category
</label>
</div>
}
</div>
</div></div>
<div :class="{ 'opacity-50 pointer-events-none': schedule !== 'Weekly' }" class=" mt-2 border-neutral-400 dark:border-neutral-600 border rounded-lg" >
<h4
class="px-5 py-1 bg-neutral-500 bg-opacity-10 rounded-lg font-body text-primary dark:text-white w-full flex justify-between items-center cursor-pointer"
>
<span class="flex flex-row items-center space-x-1 ">
Day of Week to Send On
</span>
</h4>
<div class="grid grid-cols-3 sm:grid-cols-[repeat(auto-fit,minmax(80px,1fr))] my-2 w-full lg:w-1/2" x-show="schedule === 'Weekly'">
@foreach (var day in Model.DaysOfWeek)
{
var checkedDay = day.ToString() == Model.Day ? "checked" : "";
<div class="flex items-center w-auto h-full min-h-[50px]">
<input id="@day" type="radio" value="@day" name="day" @checkedDay class="hidden peer">
<label for="@day" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@day.ToString()
</label>
</div>
}
</div>
</div>
<div :class="{ 'opacity-50 pointer-events-none': schedule !== 'Monthly' }" class=" mt-2 border-neutral-400 dark:border-neutral-600 border rounded-lg" >
<h4
class="px-5 py-1 bg-neutral-500 bg-opacity-10 rounded-lg font-body text-primary dark:text-white w-full flex justify-between items-center cursor-pointer">
<span class="flex flex-row items-center space-x-1 ">
Day of Month to Send On
</span>
</h4>
<div class="grid grid-cols-[repeat(auto-fit,minmax(35px,1fr))] w-full mx-2" x-show="schedule === 'Monthly'">
@for(int i=1; i<32; i++)
{
var checkedMonthDay = i == Model.DayOfMonth ? "checked" : "";
<div class="flex items-center w-auto my-2 h-full min-h-[35px]">
<input id="Day_@i" type="radio" value="@i" name="daypfmonth" @checkedMonthDay class="hidden peer">
<label for="Day_@i" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@i.GetOrdinal()
</label>
</div>
}
</div>
</div>
@* Action Buttons *@
<div class="flex flex-row gap-2 mt-4">
<button type="submit" class="btn btn-primary">Subscribe</button>
<button type="reset" class="btn-warning btn">Reset</button>
</div>
</div>
</form>
<div class="grid grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))] w-full lg:w-1/3">
@{
var frequency = Enum.GetValues(typeof(SubscriptionType)).Cast<SubscriptionType>().ToList();
}
@foreach (var freq in frequency)
{
<div class="flex items-center w-auto h-full min-h-[30px] lg:mb-0 mb-3">
<input x-model="schedule" id="@freq" type="radio" value="@freq.ToString()" name="SubscriptionType" class="hidden peer">
<label for="@freq" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@freq.EnumDisplayName()
</label>
</div>
}
</div>
Sie können sehen, dass dies CSS basiert, mit dem Tailwind CSS-Framework peer
CSS-Dienstprogramm, um festzulegen, dass beim Anklicken des Etiketts die geprüfte Eigenschaft der Eingabe eingestellt und das Styling geändert werden soll.
Ich nutze dies später auf der Seite, um zu bestimmen, welcher Selector (Wochentag / Tag des Monats) den Benutzern zur Verfügung steht und welche Elemente die Auswahl erlauben.
<div :class="{ 'opacity-50 pointer-events-none': schedule !== 'Monthly' }" class=" mt-2 border-neutral-400 dark:border-neutral-600 border rounded-lg" >
<h4
class="px-5 py-1 bg-neutral-500 bg-opacity-10 rounded-lg font-body text-primary dark:text-white w-full flex justify-between items-center cursor-pointer">
<span class="flex flex-row items-center space-x-1 ">
Day of Month to Send On
</span>
</h4>
<div class="grid grid-cols-[repeat(auto-fit,minmax(35px,1fr))] w-full mx-2" x-show="schedule === 'Monthly'">
@for(int i=1; i<32; i++)
{
var checkedMonthDay = i == Model.DayOfMonth ? "checked" : "";
<div class="flex items-center w-auto my-2 h-full min-h-[35px]">
<input id="Day_@i" type="radio" value="@i" name="daypfmonth" @checkedMonthDay class="hidden peer">
<label for="Day_@i" class="ml-2 text-sm font-medium text-white
bg-blue-dark border-gray-light border rounded-xl px-1 py-2 w-full
peer-checked:text-blue-600 peer-checked:dark:bg-green text-center justify-center">
@i.GetOrdinal()
</label>
</div>
}
</div>
</div>
Sie können sehen, dass ich eine Alpine.js css Template-Klasse habe, die die Deckkraft des Elements auf 50% setzt und Zeigerereignisse deaktiviert, wenn der Zeitplan nicht auf Monthly gesetzt ist. Dies ist eine einfache Möglichkeit, Elemente zu verstecken, die nicht benötigt werden.
:class="{ 'opacity-50 pointer-events-none': schedule !== 'Monthly' }"
Es versteckt auch den Monethly Tag Selektor, wenn der Zeitplan nicht auf Monthly gesetzt ist.
x-show="schedule === 'Monthly'"
Das ist also der Witz auf der Planseite. Ich kümmere mich im nächsten Post um das Backend.
Im nächsten Beitrag poste ich die neuen strukturellen Änderungen an dem Projekt, damit ich eine separate Web-Anwendung für Hanfgire und den E-Mail-Sendungsdienst verwenden kann. Dies ist eine wesentliche Veränderung, da ich von einem einzigen Projekt gehe Mostlylucid
für eine Reihe von Projekten, die die gemeinsame Nutzung von Dienstleistungen ermöglichen:
Mostlylucid
- Das Haupt-Website-Projekt.
Mostlylucid.SchedulerService
- Dies ist das Haupt-Hangfire-Projekt, das die Datenbank übernimmt, die E-Mails erstellt und sendet.
Mostlylucid.Services
- Wo die Dienste leben, die Daten zu den Top-Level-Projekten zurückgeben
Mostlylucid.Shared
- Helfer und Konstanten, die von allen Projekten verwendet werden.
Mostlylucid.DbContext
- Der Datenbankkontext für das Projekt.
Sie können sehen, dass dies der Systemarchitektur deutlich mehr Komplexität verleiht; in diesem Fall ist es jedoch notwendig, das Projekt wartungsfähig und skalierbar zu halten. Ich berichte, wie ich das in der restlichen Serie gemacht habe.
Ich habe noch eine TON Arbeit zu erledigen, um all dies geschehen zu lassen. Das Refactoring ist etwas komplex, da es mehrere Ebenen zum System hinzufügt (und Konzepte wie DTOs zum Projekt), aber ich denke, es lohnt sich auf lange Sicht.