A Newsltter Subscription Service Pt. 1 - Vereisten en abonnementspagina (Nederlands (Dutch))

A Newsltter Subscription Service Pt. 1 - Vereisten en abonnementspagina

Comments

NOTE: Apart from English (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

//

11 minute read

Inleiding

Terwijl het doorlezen van blogs van andere mensen Ik merkte dat veel van hen hebben een abonnement dienst die gebruikers in staat stelt om zich aan te melden om een e-mail naar hen wekelijks met de berichten van die blog. Ik besloot om mijn eigen versie van dit te implementeren en te delen hoe ik het deed.

OPMERKING: Ik verwacht niet dat iemand dit daadwerkelijk zal gebruiken, ik heb veel tijd op mijn handen na een contract doorgevallen dus dit houdt me bezig!

Stappen

Dus om deze service te creëren besloot ik over de volgende vereisten.

  1. Een flexibele abonnementspagina die gebruikers enige flexibiliteit biedt.
    1. Gebruikers zetten alleen e-mail.
    2. De mogelijkheid tom select taal van de e-mail.
    3. De mogelijkheid om de frequentie van de e-mail te selecteren.
      • Als maandelijks selecteert u de dag van de maand is het verzenden.
      • Als Weekly selecteert u de dag van de week die het wordt verzonden.
      • Dagelijks toestaan (tijd van de dag is niet belangrijk).
      • Autoposting toestaan om een mail te versturen wanneer ik blog.
    4. Laat de gebruiker de categorieën selecteren waarin hij geïnteresseerd is
  2. Eenvoudige afmelding toestaan
  3. Laat een voorbeeld van de mail die ze ontvangen
  4. Laat de gebruiker te allen tijde hun voorkeuren wijzigen.
  5. Een aparte dienst die de verzending van e-mails regelt.
    • Deze service zal worden aangeroepen door de blog service wanneer een nieuwe post wordt gemaakt.
    • Deze dienst zal dan de e-mail naar alle abonnees.
    • Het zal Hangfire gebruiken om het verzenden van e-mails te plannen.

De impact hiervan is dat ik nog veel werk te doen heb.

De Abonnementspagina

Ik begon met het leuke deel, het schrijven van een abonnementspagina. Ik wilde dat dit goed te werken op desktop, evenals mobiele browsers (en zelfs de SPA); van my Umami analytics Ik zie een redelijk deel van de gebruikers toegang tot deze roer vanaf mobiele apparaten.

Umamu-platforms

Ik wilde ook dat de Abonnement pagina duidelijk te zijn hoe te gebruiken; Ik ben een grote gelovige in Steve Krug's "Don't make me think" filosofie waar een pagina duidelijk te gebruiken moet zijn en niet vereist dat de gebruiker na te denken over hoe het te gebruiken.

Dit betekent dat de standaard zou moeten de meerderheid van de gebruikers willen gebruiken. Ik besloot over de volgende wanbetalingen:

  1. Wekelijkse e-mails
  2. Engelse taal
  3. Alle categorieën geselecteerd
  4. E-mail verzonden op een maandag

Ik kan dit natuurlijk later veranderen als deze onjuist blijken te zijn.

Dit is de pagina die ik bouwde:

Abonnementspagina

De Code voor de pagina

Net als bij de rest van deze site wilde ik de code zo eenvoudig mogelijk maken. Het gebruikt de volgende HTML:

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>
Je kunt zien dat dit vrij eenvoudig is als het gaat. Het maakt gebruik van Alpine.js om alle gebruikersinteracties en een gemeenschappelijk UI-element voor alle selecties te verwerken.
<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>

U kunt zien dat dit is CSS gebaseerd, met behulp van de Tailwind CSS framework's peer CSS-hulpprogramma om aan te geven dat wanneer het label wordt geklikt, de door de invoer aangevinkte eigenschap moet worden ingesteld en de styling moet worden gewijzigd.

Ik gebruik dit later in de pagina om te bepalen welke selector (Weekdag / Maanddag) beschikbaar te stellen aan gebruikers en de elementen te tonen die selectie mogelijk maken.

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

U kunt zien dat ik een Alpine.js css sjabloon klasse heb die de opaciteit van het element op 50% zet en pointer events deactiveert als het schema niet op Maandelijks is ingesteld. Dit is een eenvoudige manier om elementen te verbergen die niet nodig zijn.

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

Het verbergt ook de Monetische dagkiezer als het schema niet op Maandelijks is ingesteld.

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

Dus dat is de puinhoop van de Schedule pagina. Ik dek de backend in de volgende post.

Volgende stappen

In de volgende post zal ik (wanneer ik klaar ben met bulden) post de nieuwe structurele wijzigingen aan het project om me in staat te stellen om een aparte webtoepassing voor Hanfgire en de e-mail verzenden service te gebruiken. Dit is een substantiële verandering als ik ga van een enkel project Mostlylucid voor een aantal projecten die het mogelijk maken diensten te delen:

  1. Mostlylucid - Het hoofdproject van de website.

  2. Mostlylucid.SchedulerService - Dit is het belangrijkste Hangfire project dat de Database zal doorboren, de e-mails zal bouwen en ze zal versturen.

  3. Mostlylucid.Services - Waar de diensten wonen die gegevens teruggeven aan de top projecten

  4. Mostlylucid.Shared - Helpers en Constanten gebruikt door alle projecten.

  5. Mostlylucid.DbContext - De database context voor het project.

Je kunt zien dat dit de systeemarchitectuur aanzienlijk complexer maakt; maar in dit geval is het noodzakelijk om het project onderhoudbaar en schaalbaar te houden. Ik zal uitleggen hoe ik dit deed in de rest van deze serie.

Conclusie

Ik heb nog een TON werk te doen om dit allemaal te laten gebeuren. De refactoring is wat complex omdat het meerdere lagen aan het systeem toevoegt (en concepten zoals DTO's aan het project) maar ik denk dat het de moeite waard is op de lange termijn.

logo

©2024 Scott Galloway