Back to "Служба підписки новин 1 - Вимоги і підписка на сторінку"

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

Служба підписки новин 1 - Вимоги і підписка на сторінку

Saturday, 21 September 2024

Вступ

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

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

Кроки

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

  1. Сторінка гнучкої підписки, яка надає користувачам можливість гнучкості.
    1. Користувачі долучаються лише до електронної пошти.
    2. Можливість вибору мови електронної пошти.
    3. Можливість вибору частоти повідомлень електронної пошти.
      • Якщо щомісяця вибере день місяця, його буде надіслано.
      • Якщо щотижня вибере день тижня, його буде надіслано.
      • Дозволяти щодня (час дня не є важливим).
      • Дозволити автоматичне надсилання повідомлень під час створення блогу.
    4. Дозволити користувачеві вибрати категорії, в яких вони зацікавлені
  2. Дозволити просту підписку на підписку
  3. Дозволити попередній перегляд пошти, яку вони отримають
  4. Дозволити користувачеві змінювати їх параметри у будь- який час.
  5. Окрема служба, яка керує надсиланням повідомлень електронної пошти.
    • Цю службу буде викликано службою блогу під час створення нового допису.
    • Після цього ця служба надішле повідомлення електронної пошти всім передплатникам.
    • За допомогою Hangfire можна запланувати відправку повідомлень електронної пошти.

Наслідок цього в тому, що у мене багато роботи.

Сторінка підписка

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

Платформи umamu

Я також хотів, щоб сторінка передплат була очевидною як користуватися; я великий віруючий в Стив Крюг "Не заставляй меня думать" Філософія, де сторінка повинна бути очевидною для використання і не вимагає від користувача думати про те, як нею користуватися.

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

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

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

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

Сторінка передплати

Код сторінки

Так само, як і з рештою сайту, я хотів зробити код якомога простішим. Він використовує такий 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>
Ви бачите, що все досить просто. Він використовує альпійські.js для роботи з усіма користувачами і загальний елемент інтерфейсу користувача для всіх відборів.
<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 Tailwin peer Утиліта CSS, щоб вказати, що, коли на ньому натиснуто мітку, слід встановити перевірену властивість вхідних даних і змінити її стиль.

Я використовую це пізніше на сторінці, щоб визначити, який вибір вибору (День/ День місяця), щоб зробити доступним для користувачів і показати елементи, які надають можливість вибору.

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

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

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

Крім того, програма ховає діалогове вікно вибору дня Монтлі, якщо розклад не встановлено у значення " Щомісячний."

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

Отже, це джост сторінки розкладу. Я покрою сервер в следующем помещении.

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

На наступному посту я (коли я закінчу опублікувати його) розміщуватиму нові структурні зміни в проекті, щоб дозволити мені використовувати окрему веб-програму для Hansfgire та служби надсилання пошти. Це істотна зміна з одного проекту. Mostlylucid на декілька проектів, які надають змогу спільно використовувати служби:

  1. Mostlylucid - Головний веб-проект.

  2. Mostlylucid.SchedulerService - Это главный проект Hangfire, который перенесет базу данных, соберет письма и отправит их.

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

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

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

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

Включення

Я всё ещё работаю, чтобы всё это произошло. Реорганізація є дещо складною, оскільки вона включає в себе додавання декількох шарів до системи (і концепцій, таких як DTO до проекту), але я думаю, що вона варта в довгостроковій перспективі.

logo

©2024 Scott Galloway