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
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ä!
Joten luodakseni tämän palvelun päätin seuraavista vaatimuksista.
Tämä vaikuttaa siihen, että minulla on paljon työtä tehtävänä.
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.
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:
Voin tietenkin muuttaa asian myöhemmin, jos se osoittautuu vääräksi.
Joten tämä on sivu, jonka päädyin rakentamaan:
Kuten muutkin sivustot halusin tehdä koodista mahdollisimman yksinkertaisen. Se käyttää seuraavaa 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>
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ä.
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:
Mostlylucid
- Pääsivuston projekti.
Mostlylucid.SchedulerService
- Tämä on tärkein Hangfire-projekti, joka ässää tietokantaa, rakentaa sähköpostit ja lähettää ne.
Mostlylucid.Services
- Missä palvelut asuvat, jotka palauttavat tiedot huipputason projekteihin
Mostlylucid.Shared
- Kaikissa projekteissa käytetään auttajia ja konstaattoreita.
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.
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.