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
بينما كنت أتصفح مدونات أشخاص آخرين لاحظت أن الكثير منهم لديهم خدمة اشتراكية تسمح للمستخدمين بالتوقيع على إرسال بريد إلكتروني لهم أسبوعياً مع نشرات من تلك المدونة. قررت تنفيذ نسختي الخاصة من هذا و أشارك كيف فعلت ذلك.
ملاحظة: لا أتوقع أن يستخدم أي أحد هذا فعلاً، لدي الكثير من الوقت على يدي بعد أن وقع العقد
لذا لإنشاء هذه الخدمة قررت على المتطلبات التالية.
إن تأثير هذا هو أن لدي الكثير من العمل للقيام به.
بدأت مع الجزء الممتع، كتابة صفحة الاشتراك. أردت لهذا أن يعمل بشكل جيد على سطح المكتب وكذلك على متصفحات الهواتف النقالة (وحتى جنوب جنوب جنوب جنوب(ج) من المسائل التحليلية المتعلقة بأميامي أستطيع أن أرى نسبة عادلة من المستخدمين الوصول إلى هذه الحركة من الأجهزة المحمولة.
كما أردت أيضاً أن تكون صفحة الإشتراك واضحة كيفية استخدامها، أنا مؤمن كبير في (ستيف كروج) "لا تجعلني أفكر" فلسفة حيث يجب أن تكون الصفحة واضحة للاستخدام وليس مطالبة المستخدم للتفكير في كيفية استخدامها.
وهذا يعني أنه ينبغي أن تكون القيم الافتراضية هي غالبية المستخدمين الذين سيرغبون في استخدامها. قرّرتُ على الإفتراضات التالية:
ويمكنني بالطبع أن أغير هذا لاحقا إذا ثبت أن هذه غير صحيحة.
إذاً هذه هي الصفحة التي انتهيت من بنائها:
كما هو الحال مع بقية هذا الموقع أردت أن أجعل الرمز بسيطاً قدر الإمكان وهو يستخدم 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>
يمكنك أن ترى أن هذا مبني على CSSS، باستخدام إطار Tailwind CSS peer
س س س إلى تحديد مُنتَزَر على المُشَارَعَة يجب أن تُعِدّ خِصْيَّة المُدخِل المُحَكَّمَة و تُغيِّر مُشَغِّلَتها.
استخدم هذا لاحقاً في الصفحة لتحديد أي الانتقاء (يوم الأسبوع/يوم الشهر) لجعله متاحاً للمستخدمين إظهار العناصر التي تسمح بالانتقاء.
<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>
يمكنك أن ترى أن لدي Alpin.js css comp صنف يحدد عدم وضوح العنصر إلى 50% و مؤشر تعطيل الأحداث إذا لم يكن الجدول مُحدداً للشهرية. وهذه طريقة بسيطة لإخفاء العناصر التي لا حاجة إليها.
:class="{ 'opacity-50 pointer-events-none': schedule !== 'Monthly' }"
كما انه يخفي مختار يوم الجبلي اذا لم يكن الجدول مبرمجا للشهرية.
x-show="schedule === 'Monthly'"
اذاً هذا هو jist لصفحة الجدول سأغطّي الخلفية في الموقع التالي.
في المقال التالي سأقوم (عندما أنهي مسحه) بوضع التغييرات الهيكلية الجديدة في المشروع للسماح لي باستخدام تطبيق منفصل على شبكة الإنترنت لـ Hanfgire وخدمة إرسال البريد الإلكتروني. هذا تغيير كبير عندما انتقل من مشروع واحد Mostlylucid
عدد من المشاريع التي تسمح بتقاسم الخدمات:
Mostlylucid
- مشروع الموقع الرئيسي.
Mostlylucid.SchedulerService
- هذا هو مشروع "هالفاير" الرئيسي الذي سيدعم قاعدة البيانات، ويبني البريد الإلكتروني ويرسلها.
Mostlylucid.Services
- مكان إقامة الخدمات التي تعيد البيانات إلى مشاريع المستوى الأعلى
Mostlylucid.Shared
- المساعدة والثوابت المستخدمة في جميع المشاريع.
Mostlylucid.DbContext
- قاعدة بيانات سياق المشروع.
يمكنكم أن تروا أن هذا يضيف تعقيداً أكبر بكثير إلى بنية النظام، لكن في هذه الحالة من الضروري إبقاء المشروع قابلاً للصيانة والتعديل. سأغطي كيف فعلت هذا في بقية هذه السلسلة.
لا يزال لدي الكثير من العمل للقيام به لجعل كل هذا يحدث. إعادة التصنيع معقدة إلى حد ما لأنها تنطوي على إضافة طبقات متعددة إلى النظام (ومفاهيم مثل DTOs إلى المشروع) ولكن أعتقد أنه يستحق ذلك على المدى الطويل.