NOTE: Apart from
(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
Переглядаючи блоги інших людей, я помітив, що багато з них мають службу підписки, яка дозволяє користувачам підписувати повідомлення щотижня з постами цього блогу. Я вирішив реалізувати свою власну версію і поділитися тим, як я це зробив.
Я не очікую, що хтось це використає, у мене з'явилося багато часу після того, як укладено контракт.
Отже, щоб створити цю службу, я вирішив про наступні вимоги.
Наслідок цього в тому, що у мене багато роботи.
Я начал с веселой частью, написал страницу для подписи. Я хотів, щоб це працювало добре на стільниці, а також мобільних браузерів (і навіть на SPA); від аналітичні міммі Я бачу досить багато користувачів, які отримують доступ до цього мотлоху з мобільних пристроїв.
Я також хотів, щоб сторінка передплат була очевидною як користуватися; я великий віруючий в Стив Крюг "Не заставляй меня думать" Філософія, де сторінка повинна бути очевидною для використання і не вимагає від користувача думати про те, як нею користуватися.
Це означає, що типовим значенням має бути більшість користувачів. Ось деякі з них:
Я, звичайно, можу змінити це пізніше, якщо це виявиться хибним.
Ось сторінка, яку я збудував.
Так само, як і з рештою сайту, я хотів зробити код якомога простішим. Він використовує такий HTML:
@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 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
на декілька проектів, які надають змогу спільно використовувати служби:
Mostlylucid
- Головний веб-проект.
Mostlylucid.SchedulerService
- Это главный проект Hangfire, который перенесет базу данных, соберет письма и отправит их.
Mostlylucid.Services
- Де живуть служби, які повертають дані до проектів найвищого рівня
Mostlylucid.Shared
- Допомагачі і Константи, які використовують усі проекти.
Mostlylucid.DbContext
- Контекст бази даних проекту.
Ви можете бачити, що це додає значно більше складності системній архітектурі; але в цьому випадку необхідно підтримувати і масштабувати проект. Я розповім, як я це зробив у решті цих статей.
Я всё ещё работаю, чтобы всё это произошло. Реорганізація є дещо складною, оскільки вона включає в себе додавання декількох шарів до системи (і концепцій, таких як DTO до проекту), але я думаю, що вона варта в довгостроковій перспективі.