Back to "Newsletter Υπηρεσία συνδρομής Pt. 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

Newsletter Υπηρεσία συνδρομής Pt. 1 - Απαιτήσεις και Σελίδα Συνδρομής

Saturday, 21 September 2024

Εισαγωγή

Κατά τη διάρκεια της χρήσης blog άλλων ανθρώπων παρατήρησα ότι πολλά από αυτά έχουν μια υπηρεσία συνδρομής που επιτρέπει στους χρήστες να εγγραφεί για να έχουν ένα email που αποστέλλονται σε αυτούς κάθε εβδομάδα με τις δημοσιεύσεις από αυτό το blog. Αποφάσισα να εφαρμόσω τη δική μου εκδοχή για αυτό και να μοιραστώ τον τρόπο με τον οποίο το έκανα.

ΣΗΜΕΙΩΣΗ: Δεν περιμένω κανείς θα χρησιμοποιήσει πραγματικά αυτό, έχω πολύ χρόνο στα χέρια μου μετά από ένα συμβόλαιο έπεσε μέσα έτσι αυτό με κρατά απασχολημένο!

Βήματα

Έτσι, για να δημιουργήσω αυτή την υπηρεσία αποφάσισα τις ακόλουθες απαιτήσεις.

  1. Μια ευέλικτη σελίδα συνδρομής που επιτρέπει στους χρήστες κάποια ευελιξία.
    1. Οι χρήστες βάζουν μόνο email.
    2. Η δυνατότητα να επιλέξετε γλώσσα του ηλεκτρονικού ταχυδρομείου.
    3. Η ικανότητα επιλογής της συχνότητας του email.
      • Εάν το μήνα επιλέξετε την ημέρα του μήνα που αποστέλλεται.
      • Αν το Weekly επιλέξει την ημέρα της εβδομάδας στέλνεται.
      • Επιτρέπεται καθημερινά (η ώρα της ημέρας δεν είναι σημαντική).
      • Επιτρέπετε την αυτόματη ανάρτηση για να στείλετε ένα μήνυμα κάθε φορά που blog.
    4. Αφήστε το χρήστη να επιλέξει τις κατηγορίες που ενδιαφέρονται για
  2. Επιτρέπετε απλή ανεπιγραφή
  3. Επιτρέπετε την προεπισκόπηση της αλληλογραφίας που θα λάβουν
  4. Αφήστε το χρήστη να αλλάξει τις προτιμήσεις του ανά πάσα στιγμή.
  5. Μια ξεχωριστή υπηρεσία που χειρίζεται την αποστολή μηνυμάτων ηλεκτρονικού ταχυδρομείου.
    • Αυτή η υπηρεσία θα καλείται από την υπηρεσία blog κάθε φορά που γίνεται μια νέα ανάρτηση.
    • Αυτή η υπηρεσία θα στείλει το email σε όλους τους συνδρομητές.
    • Θα χρησιμοποιήσει Hangfire για να προγραμματίσει την αποστολή των μηνυμάτων ηλεκτρονικού ταχυδρομείου.

Ο αντίκτυπος είναι ότι έχω πολλή δουλειά να κάνω.

Η Σελίδα της Συνδρομής

Ξεκίνησα με το διασκεδαστικό μέρος, γράφοντας μια σελίδα συνδρομής. Ήθελα αυτό να λειτουργήσει καλά στην επιφάνεια εργασίας, καθώς και κινητά προγράμματα περιήγησης (και ακόμη και το SPA), από Μου Umami analytics Μπορώ να δω μια δίκαιη αναλογία των χρηστών πρόσβαση σε αυτό το σάλο από τις κινητές συσκευές.

Πλατφόρμες Umamu

Ήθελα επίσης η σελίδα συνδρομής να είναι προφανής πώς να χρησιμοποιήσετε? Είμαι ένας μεγάλος πιστός σε Το "Μη με κάνεις να σκεφτώ" του Στιβ Κρουγκ. φιλοσοφία όπου μια σελίδα θα πρέπει να είναι προφανής στη χρήση και να μην απαιτούν από τον χρήστη να σκεφτεί πώς να τη χρησιμοποιήσει.

Αυτό σημαίνει ότι οι αθετήσεις θα πρέπει να είναι η πλειοψηφία των χρηστών που θα θέλουν να χρησιμοποιήσουν. Αποφάσισα για τις ακόλουθες αθετήσεις:

  1. Εβδομαδιαία emails@ info: whatsthis
  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>
Μπορείτε να δείτε ότι αυτό είναι αρκετά απλό όπως πηγαίνει. Χρησιμοποιεί Alpine.js για να χειριστεί όλες τις αλληλεπιδράσεις των χρηστών και ένα κοινό στοιχείο UI για όλες τις επιλογές.
<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 χρησιμότητα για να καθορίσετε ότι όταν η ετικέτα είναι κλικ σε αυτό θα πρέπει να ρυθμίσετε την ελεγμένη ιδιοκτησία της εισόδου και να αλλάξετε το στυλ της.

Χρησιμοποιώ αυτό αργότερα στη σελίδα για να καθορίσω ποιος επιλογέας (Week Day / Day of Month) για να είναι διαθέσιμος στους χρήστες και να δείξει τα στοιχεία που επιτρέπουν την επιλογή.

<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% και απενεργοποιεί τα γεγονότα δείκτη εάν το πρόγραμμα δεν είναι ρυθμισμένο σε Μηνιαία. Αυτός είναι ένας απλός τρόπος για να κρύψουμε στοιχεία που δεν χρειάζονται.

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

Κρύβει επίσης τον επιλογέα της ημέρας Monethly εάν το πρόγραμμα δεν είναι ρυθμισμένο σε Μηνιαία.

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

Ώστε αυτή είναι η ιδέα της σελίδας του προγράμματος. Θα καλύψω το πίσω μέρος στην επόμενη θέση.

Επόμενα βήματα

Στην επόμενη ανάρτηση θα (όταν τελειώσω το bludding) δημοσιεύσετε τις νέες διαρθρωτικές αλλαγές στο έργο για να μου επιτρέψετε να χρησιμοποιήσω μια ξεχωριστή web εφαρμογή για Hanfgire και την υπηρεσία αποστολής ηλεκτρονικού ταχυδρομείου. Αυτή είναι μια σημαντική αλλαγή καθώς προέρχομαι από ένα και μόνο έργο Mostlylucid σε ορισμένα έργα που επιτρέπουν την ανταλλαγή υπηρεσιών:

  1. Mostlylucid - Το κύριο έργο της ιστοσελίδας.

  2. Mostlylucid.SchedulerService - Αυτό είναι το κύριο έργο Hangfire που θα χρησιμοποιήσει τη βάση δεδομένων, θα κατασκευάσει τα email και θα τα στείλει.

  3. Mostlylucid.Services - Όπου ζουν οι υπηρεσίες που επιστρέφουν δεδομένα στα έργα υψηλού επιπέδου

  4. Mostlylucid.Shared - Βοηθοί και Σταθεροί που χρησιμοποιούνται από όλα τα προγράμματα.

  5. Mostlylucid.DbContext - Το πλαίσιο της βάσης δεδομένων για το έργο.

Μπορείτε να δείτε ότι αυτό προσθέτει σημαντικά περισσότερη πολυπλοκότητα στην αρχιτεκτονική του συστήματος, αλλά σε αυτή την περίπτωση είναι απαραίτητο να διατηρηθεί το έργο διατηρήσιμο και κλιμακωτό. Θα καλύψω το πώς το έκανα αυτό στην υπόλοιπη σειρά.

Συμπέρασμα

Έχω ακόμα έναν τόνο δουλειάς να κάνω για να συμβεί όλο αυτό. Ο αναπροσανατολισμός είναι κάπως περίπλοκος καθώς περιλαμβάνει την προσθήκη πολλαπλών στρωμάτων στο σύστημα (και έννοιες όπως DTOs στο έργο) αλλά νομίζω ότι αξίζει μακροπρόθεσμα.

logo

©2024 Scott Galloway