Back to "Услуга подписки на newsletter Pt. M SK1 | | - | Виmaganня та сторінка подписки"

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

Услуга подписки на newsletter Pt. M SK1 | | - | Виmaganня та сторінка подписки

Saturday, 21 September 2024

Введение

Коли я переглядав блоги інших людей, я помітив, що багато з них мають систему abonamentу, яка дозволяє користувачам підписатись і надіслати їм еженедельну електронну пошту з постами з цього блогу. Я вирішив реалізувати свою власну версію цього і поділитися, як я це зробив.

NOTE: Я не очікую, що хтось дійсно використовуватиме це. , У мене багато часу після того, як контракт провалився, тож це змушує мене зайнятись

Шаг

Отже, щоб створити цю службу я вирішила щодо наступних вимог.

  1. Гнучка сторінка подписки, яка дає користувачам певну гнучкість.
    1. Пользовачі вводять тільки електронні листи.
    2. Możliwość вибору мови електронного листа.
    3. Możliwość вибору частоти електронної листи.
      • Якщо місяць вибирає день місяця, то
      • Якщо Щотижня вибрала день тижня, то
      • Дозволяти щоденно (час дня неважливийM SK1
      • Дозволять автоматично відправляти повідомлення, коли я пишу блог.
    4. Дозволяти користувачам вибрати категорії, які їх цікавлять
  2. Дозволяти простим відмовитися від підпису
  3. Дозволяти перед переглядом листів, які вони отримають
  4. Дозволяти користувача змінювати свої preference в будь-який час.
  5. Інша служба, яка працює з надсиланням електронних листів.
    • Ця служба буде викликана службою блогу, коли створюється новий пост.
    • Ця служба відправить повідомлення усім абонентам.
    • Він використовуватиме Hangfire для планування відправлення електронних листів.

Наслідком цього є те, що я маю багато роботи.

Веб-сторінка подписки

Я почав з кумедної частини, написання сторінки abonamentу . Я хотів, щоб це працювало добре на робочому столі, а також на мобільних браузерах ( і навіть SPA); від мій аналіз Умамі Я бачу, що з мобільних пристроїв доступ до цього сигналу досягає значний відсоток користувачів

Платформа Умаму

Я також хотів, щоб сторінка подписки була очевидною, як використовувати Стів Круг: "" МSK1, МSK2 - це не змусить мене думати філософія, де сторінка має бути очевидною для використання, а не обов 'язково мусить думати про те, як її використовувати

Це означає, що погані варіанти мають бути для більшості користувачів.

  1. Щотижня
  2. Англійська мова
  3. Вибрані всі категорії
  4. Е-mail відправлено в понеділок

Звичайно, я можу це змінити пізніше, якщо це виявиться неправильним.

Ось сторінка, яку я збудував.

Список

Код для сторінки

Як і з усіма іншими сайтами, я хотів зробити код якомога простішим. Він використовує наступні 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>
Ви бачите, що це досить просто:
<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>

Ви бачите, що це базується на CSS,, використовуючи структуру CSS Tailwind peer CSS утилітація, яка визначає, що при натисненні літери на неї треба ввести property checked ' s і змінити її

Я використовую це пізніше на сторінці, щоб визначити, який відбірник (Week Day МSK1 Day of MonthM SK2 зробити доступним користувачам & show the elements allowing selection.

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

Ви бачите, що я маю класику шаблону Alpine.js css, яка налаштовує прозорість елементу до 50% і відключає події вказівника, якщо графік не налаштований на щомісяця . МSK2 Це простий спосіб ховати непотрібні елементи.

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

Він також приховує відбірник Monethly дня, якщо графік не влаштований на Monthly.

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

Так що МСК0 , як написано на сторінці График , ММСК1 , я MМСК2 , отримаю заголовок в наступному листі .

Наступні кроки

У наступному листі я'll ( коли я закінчу згортати йогоM SK2 опублікую нові структурні зміни в проекті, щоб дозволити мені використовувати окрему веб-аplikaцію для Hanfgire і службу відправлення електронної пошти . Це суттєва зміна, тому що я починаю з одного проекту Mostlylucid для деяких проектів, що дозволяють обмінюватися послугами:

  1. Mostlylucid - головний проект веб-сайтуM SK1

  2. Mostlylucid.SchedulerService - Це головний проект Hangfire, який перейде до бази даних

  3. Mostlylucid.Services - Де знаходяться послуги, які повертають дані до проектів верхнього рівня

  4. Mostlylucid.Shared - Помоги та константи, які вживаються у всіх проектах

  5. Mostlylucid.DbContext - Контекст бази даних для проекту

Ви бачите, що це додає значно більшої складності системній архітектурі. I' Я розповім, як я це зробив в останню частину цієї серії

На завершення

Я все ще маю багато роботи, щоб все це сталося. МСК0 Переробка дещо складна, бо вона включає в себе доdanie кількох шарів до системи ( і концепції, як-от DTO до проекту ) але я думаю, що це заслужило б зусиль на довгостроковій перспективі

logo

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