Newsletter-tilauspalvelu pt. 1 - Vaatimukset ja tilaussivu (Suomi (Finnish))

Newsletter-tilauspalvelu pt. 1 - Vaatimukset ja tilaussivu

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

//

Less than a minute

Johdanto

Kun tutkin muiden ihmisten blogeja, huomasin, että monilla heistä on maksullinen palvelu, jonka kautta käyttäjät voivat ilmoittautua sähköpostilla, joka lähetetään heille viikoittain kyseisen blogin viestien kanssa. Päätin toteuttaa oman versioni tästä ja kertoa, miten tein sen.

HUOMAUTUS: En odota, että kukaan todella käyttää tätä, minulla on paljon aikaa sopimuksen kaaduttua, joten tämä pitää minut kiireisenä!

Vaiheet

Joten luodakseni tämän palvelun päätin seuraavista vaatimuksista.

  1. Joustava tilaussivu, joka antaa käyttäjille jonkin verran joustavuutta.
    1. Käyttäjät vain lähettävät sähköpostia.
    2. Kyky valita sähköpostin kieli.
    3. Kyky valita sähköpostin taajuus.
      • Jos valitset kuukausittain sen kuukauden päivän, jonka se on lähetetty.
      • Jos Weekly valitsee viikonpäivän, se lähetetään.
      • Salli päivittäin (päivällä ei ole merkitystä).
      • Anna automaattisen lähettämisen lähettää sähköposti aina, kun bloggaan.
    4. Anna käyttäjän valita kategoriat, joista hän on kiinnostunut
  2. Salli yksinkertainen unohdus
  3. Salli heidän saamansa postin esikatselu
  4. Anna käyttäjän muuttaa mieltymyksiään milloin tahansa.
  5. Erillinen palvelu, joka käsittelee sähköpostien lähettämistä.
    • Bloggauspalvelu soittaa tähän palveluun aina, kun uusi viesti tehdään.
    • Tämän jälkeen tämä palvelu lähettää sähköpostin kaikille tilaajille.
    • Se käyttää Hangfireä sähköpostien lähettämisen aikataulussa.

Tämä vaikuttaa siihen, että minulla on paljon työtä tehtävänä.

Tilaussivu

Aloitin hauskasta osasta, tilaussivun kirjoittamisesta. Halusin, että tämä toimisi hyvin sekä työpöydällä että mobiiliselaimilla (ja jopa SPA) Oma Umami-analytiikka Näen melkoisen osan käyttäjistä käyttävän tätä kiihoketta mobiililaitteista.

Umamu-alustat

Halusin myös, että tilaussivu olisi selkeä tapa käyttää; uskon vahvasti Steve Krug's "Älä laita minua ajattelemaan" Filosofia, jossa sivun tulisi olla itsestäänselvä eikä vaatia käyttäjää miettimään, miten sitä pitäisi käyttää.

Tämä tarkoittaa, että oletusten pitäisi olla suurin osa käyttäjistä haluaa käyttää. Päätin seuraavista oletuksista:

  1. Viikoittaiset sähköpostit
  2. Englannin kieli
  3. Kaikki valitut kategoriat
  4. Maanantaina lähetetty sähköposti

Voin tietenkin muuttaa asian myöhemmin, jos se osoittautuu vääräksi.

Joten tämä on sivu, jonka päädyin rakentamaan:

Tilaussivu

Sivun koodi

Kuten muutkin sivustot halusin tehdä koodista mahdollisimman yksinkertaisen. Se käyttää seuraavaa 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>
Näette, että tämä on aika yksinkertaista. Se käyttää Alppien.js-verkkoja kaikkien käyttäjien vuorovaikutukseen ja yhteiseen UI-elementtiin kaikissa valinnoissa.
<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>

Voit nähdä, että tämä on CSS-pohjainen, käyttäen Tailwind CSS -kehystä peer CSS-apuohjelma tarkentaa, että kun etikettiä napsautetaan, sen pitäisi asettaa syötteen valittu ominaisuus ja muuttaa sen muotoilua.

Käytän tätä myöhemmin sivulla määrittääkseni, mikä valitsija (viikkopäivä / kuukausipäivä) asettaa käyttäjän saataville ja näyttääkseni valinnat mahdollistavat elementit.

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

Huomaat, että minulla on alppi.js css -malliluokka, joka asettaa elementin läpinäkyvyyden 50 prosenttiin ja lamauttaa osoittintapahtumat, jos aikataulua ei ole asetettu Monthlylle. Tämä on yksinkertainen tapa piilottaa elementtejä, joita ei tarvita.

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

Se kätkee myös Monitoimipäivän valitsijan, jos aikataulua ei ole asetettu Monthlylle.

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

Se on siis aikataulusivun vitsi. Hoidan backendin seuraavassa viestissä.

Seuraavat vaiheet

Seuraavassa viestissä (kun saan pullistelun valmiiksi) lähetän projektiin uudet rakenteelliset muutokset, jotta voin käyttää Hanfgiren ja sähköpostin lähetyspalvelun erillistä verkkosovellusta. Tämä on merkittävä muutos, kun siirryn yhdestä projektista Mostlylucid Moniin hankkeisiin, jotka mahdollistavat palvelujen jakamisen:

  1. Mostlylucid - Pääsivuston projekti.

  2. Mostlylucid.SchedulerService - Tämä on tärkein Hangfire-projekti, joka ässää tietokantaa, rakentaa sähköpostit ja lähettää ne.

  3. Mostlylucid.Services - Missä palvelut asuvat, jotka palauttavat tiedot huipputason projekteihin

  4. Mostlylucid.Shared - Kaikissa projekteissa käytetään auttajia ja konstaattoreita.

  5. Mostlylucid.DbContext - Projektin tietokantakonteksti.

Tämä lisää merkittävästi järjestelmäarkkitehtuurin monimutkaisuutta, mutta tässä tapauksessa projekti on pidettävä yllä ja skaalattavissa. Kerron, miten tein tämän loppusarjassa.

Johtopäätöksenä

Minulla on vielä tonneittain töitä tämän eteen. Refactoring on melko monimutkainen, koska siihen liittyy useita kerroksia järjestelmään (ja käsitteitä, kuten ilmoitusvelvollisia projektiin), mutta mielestäni se on pitkällä aikavälillä sen arvoista.

logo

©2024 Scott Galloway