Услуга подписки на newsletter Pt. M SK1 | | - | Виmaganня та сторінка подписки
Введение
Коли я переглядав блоги інших людей, я помітив, що багато з них мають систему abonamentу, яка дозволяє користувачам підписатись і надіслати їм еженедельну електронну пошту з постами з цього блогу. Я вирішив реалізувати свою власну версію цього і поділитися, як я це зробив.
NOTE: Я не очікую, що хтось дійсно використовуватиме це. , У мене багато часу після того, як контракт провалився, тож це змушує мене зайнятись
Шаг
Отже, щоб створити цю службу я вирішила щодо наступних вимог.
- Гнучка сторінка подписки, яка дає користувачам певну гнучкість.
- Пользовачі вводять тільки електронні листи.
- Możliwość вибору мови електронного листа.
- Możliwość вибору частоти електронної листи.
- Якщо місяць вибирає день місяця, то
- Якщо Щотижня вибрала день тижня, то
- Дозволяти щоденно (час дня неважливийM SK1
- Дозволять автоматично відправляти повідомлення, коли я пишу блог.
- Дозволяти користувачам вибрати категорії, які їх цікавлять
- Дозволяти простим відмовитися від підпису
- Дозволяти перед переглядом листів, які вони отримають
- Дозволяти користувача змінювати свої preference в будь-який час.
- Інша служба, яка працює з надсиланням електронних листів.
- Ця служба буде викликана службою блогу, коли створюється новий пост.
- Ця служба відправить повідомлення усім абонентам.
- Він використовуватиме Hangfire для планування відправлення електронних листів.
Наслідком цього є те, що я маю багато роботи.
Веб-сторінка подписки
Я почав з кумедної частини, написання сторінки abonamentу . Я хотів, щоб це працювало добре на робочому столі, а також на мобільних браузерах ( і навіть SPA); від мій аналіз Умамі Я бачу, що з мобільних пристроїв доступ до цього сигналу досягає значний відсоток користувачів

Я також хотів, щоб сторінка подписки була очевидною, як використовувати Стів Круг: "" МSK1, МSK2 - це не змусить мене думати філософія, де сторінка має бути очевидною для використання, а не обов 'язково мусить думати про те, як її використовувати
Це означає, що погані варіанти мають бути для більшості користувачів.
- Щотижня
- Англійська мова
- Вибрані всі категорії
- Е-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 для деяких проектів, що дозволяють обмінюватися послугами:
Mostlylucid- головний проект веб-сайтуM SK1Mostlylucid.SchedulerService- Це головний проект Hangfire, який перейде до бази данихMostlylucid.Services- Де знаходяться послуги, які повертають дані до проектів верхнього рівняMostlylucid.Shared- Помоги та константи, які вживаються у всіх проектахMostlylucid.DbContext- Контекст бази даних для проекту
Ви бачите, що це додає значно більшої складності системній архітектурі. I' Я розповім, як я це зробив в останню частину цієї серії
На завершення
Я все ще маю багато роботи, щоб все це сталося. МСК0 Переробка дещо складна, бо вона включає в себе доdanie кількох шарів до системи ( і концепції, як-от DTO до проекту ) але я думаю, що це заслужило б зусиль на довгостроковій перспективі