Back to "En Newsletter Abo-Service Pt. 1 | - | Erforderelser och Abo-sida"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Alpine.js ASP.NET Email Newsletter

En Newsletter Abo-Service Pt. 1 | - | Erforderelser och Abo-sida

Saturday, 21 September 2024

Inleiding

När jag läste andra människors bloggar noterade jag att många av dem har en Abo-tjänster som låter kunderna skriva upp sig för att få en e-post skickad till dem varje vecka med meddelanden från den där bloggen Jag bestämde mig för att implementera min egen version av detta och dela med mig av hur jag gjorde det.

lägg märke till att jag inte förväntar mig att någon ska använda den. Jag har mycket tid på mina händer efter att ett kontrakt gått igenom, så det håller mig upptagen.

steg

Så för att skapa denna tjänst bestämde jag mig för följande krav.

  1. En flexibel ګډونsida som tillåter användaren lite flexibilitet.
    1. Användare bara lägger in e-mail.
    2. Möjligheterna att välja språket i e-posten.
    3. Möjligheterna att välja frekvensen för e-posten.
      • Om månaden väljer dagen i månaden så är det
      • Om Weekly väljer dagen i veckan så är det
      • Tillåt dagligen (dagens tid är inte viktigM SK1
      • Tillåt att automatiserad sändning skickar ett mejl närhelst jag bloggar.
    4. Tillåt användaren att välja kategorierna de är intresserade av
  2. Tillåt enkel avkommande
  3. Tillåt att previewa meddelandet de kommer få
  4. Tillåt användaren att ändra sina preferenser när som helst.
  5. En separat tjänst som hanterar att skicka e-mail.
    • Denna tjänst kommer att kallas av bloggtjänsterna närhelst en ny Beitrag görs.
    • Denna tjänst skickar sedan e-mailen till alla kunder.
    • Det kommer att använda Hangfire för att planera att skicka e-post

Inverkan av detta är att jag har mycket jobb att göra

Abonneringssidan

Jag började med det roliga: ,, skriva enabosida, ., jag ville att det fungerade bra på datorer och mobila webbsidor. SPA); från min Umami-analys Jag kan se att en rätt stor proportion av användaren får tillgång till denna röra från mobila enheter.

Umamu-plattformar

Jag ville också att Abonneringssidan skulle vara uppenbar hur man använder den. Steve Krug: "" Det får mig inte att tänka filosofi där en sida borde vara uppenbart att använda och inte kräva att användaren tänker på hur den ska användas

Det betyder att standardstandarderna borde vara de flesta av kunderna som vill använda

  1. Varje veckas e-post
  2. Engelska
  3. Alla kategorier väljade
  4. E-post skickad på en måndag

Jag kan naturligtvis ändra detta senare om de visar sig vara felaktiga.

Så det här är sidan jag byggde

Abonneringssida

Koden för sidan

Som med resten av den här sidan ville jag göra koden så enkel som möjligt.

Subscription Page
@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>
Ni kan se att detta är ganska enkelt när det går.
<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>

Ni kan se att det här är CSS-baserat, med hjälp av Tailwinds CSS ramverk peer CSS-användbar för att speciera att när etiketten är klickad på den ska sätta in insatsen's kontrollerade egenskap och ändra den

Jag använder detta senare på sidan för att avgöra vilken selektor (veckans dag | / | månadens dag ♫ ) | ska göra tillgängligt till användaren ♫

<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>

Ni kan se att jag har en Alpine.js css-templateklass som sätter elementens osynlighet till 50% och deaktiverar pekarens händelser om schemat inte är ställt till Monthly . Det här är ett enkelt sätt att gömma element som inte är nödvändiga

:class="{ 'opacity-50 pointer-events-none': schedule !== 'Monthly' }" 

Den gömmer också valaren för Monhetlig dag , om schemat inte är Monthly .

x-show="schedule === 'Monthly'"

Så att "'", det står på Schedule page , "., I, '," skall täcka baksidan i nästa artikel.

Nästa steg

I nästa artikel skriver jag: 'll (, när jag har avslutat att blöda upp den, ), lägger jag upp de nya strukturella förändringarna i projektet som låter mig använda en separat webapplikation för Hanfgire och e-posten , .. Det här är en stor förändring eftersom jag går från ett enda projekt Mostlylucid till ett antal projekt som låter dela med sig av tjänster:

  1. Mostlylucid - huvudwebsiteprojektet

  2. Mostlylucid.SchedulerService - Det här är det huvudsakliga Hangfire-projektet som kommer att gå in i databasen , bygga upp e-mailen och skicka dem

  3. Mostlylucid.Services - Var servicen bor som återför data till toppnivåprojekten

  4. Mostlylucid.Shared - Helfer och konstanter som används i alla projekt

  5. Mostlylucid.DbContext - Kontextet i databasen för projektet

Ni kan se att det här lägger till signifikant mer komplexitet i systemets arkitektur, men i detta fall är det nödvändigt att hålla projektet underhållbart och skalbart. I' ska beskriva hur jag gjorde det i resten av denna serie

I slutsats

Jag har fortfarande en ton arbete att göra för att få allt detta att hända.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.