<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucia</title>
    <description>The latest articles on DEV Community by Lucia (@lucia_alethea_d).</description>
    <link>https://dev.to/lucia_alethea_d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4183012%2Fd5c2f748-2cdc-4f6c-84b4-5355a44f1f7d.png</url>
      <title>DEV Community: Lucia</title>
      <link>https://dev.to/lucia_alethea_d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucia_alethea_d"/>
    <language>en</language>
    <item>
      <title>Understanding Blazor Components and Grid and List</title>
      <dc:creator>Lucia</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:10:51 +0000</pubDate>
      <link>https://dev.to/lucia_alethea_d/understanding-blazor-components-and-grid-and-list-4okd</link>
      <guid>https://dev.to/lucia_alethea_d/understanding-blazor-components-and-grid-and-list-4okd</guid>
      <description>&lt;h2&gt;
  
  
  1. First, understand a Blazor concept with a small demo
&lt;/h2&gt;

&lt;p&gt;Blazor lets you build interactive web interfaces with Razor components.&lt;br&gt;
A Razor component combines HTML-like markup with C# code. The markup&lt;br&gt;
defines what the user sees, while C# handles state and events.&lt;/p&gt;

&lt;p&gt;For example, imagine a small component that lets a user click a button&lt;br&gt;
to increase a number.&lt;/p&gt;

&lt;h3&gt;Click Counter Demo&lt;/h3&gt;

&lt;p&gt;You clicked the button @clickCount times.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    Click me&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;@code {&lt;br&gt;
    private int clickCount = 0;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void IncreaseCount()
{
    clickCount++;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;How this demo works&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Component state: showMessage stores whether the message should be displayed.&lt;/li&gt;
&lt;li&gt;Event handling: @onclick="ToggleMessage" runs the C# method when the button is clicked.&lt;/li&gt;
&lt;li&gt;Conditional rendering: &lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt; (showMessage) tells Blazor to render the paragraph only when the variable is true.&lt;/li&gt;
&lt;li&gt;Dynamic text: The buttonText property changes the button label according to the current state.&lt;/li&gt;
&lt;li&gt;Automatic UI update: When ToggleMessage changes showMessage, Blazor re-renders the component so the message and button label reflect the new value.
Initially, showMessage is false, so the paragraph is not displayed and the button says Show Message. Clicking the button changes the value to true, making the paragraph visible and changing the button label to Hide Message. Clicking it again hides the paragraph.
This example demonstrates how a Blazor component combines Razor markup, C# state, event handling, and conditional rendering. The demo is independent and uses only a button and a message.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Conditional Rendering Demo&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
    @buttonText&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt; (showMessage)&lt;br&gt;
{&lt;br&gt;
    &lt;/p&gt;
&lt;p&gt;&lt;br&gt;
        The message is now visible.&lt;br&gt;
    &lt;/p&gt;
&lt;br&gt;
}

&lt;p&gt;@code {&lt;br&gt;
    private bool showMessage = false;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private string buttonText =&amp;gt;
    showMessage ? "Hide Message" : "Show Message";

private void ToggleMessage()
{
    showMessage = !showMessage;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The application calls &lt;code&gt;CreateSubscriptionAsync&lt;/code&gt; with the member ID,
gym ID, membership-plan ID, start date, payment method, and terms
acceptance.&lt;/li&gt;
&lt;li&gt; The service validates the IDs, date, payment method, plan, and
membership rules.&lt;/li&gt;
&lt;li&gt; The repository checks whether the member already has an active
membership at the selected gym and counts active memberships across
all gyms.&lt;/li&gt;
&lt;li&gt; If validation succeeds, the service creates a &lt;code&gt;Subscription&lt;/code&gt; entity
and asks the repository to save it.&lt;/li&gt;
&lt;li&gt; The service returns a &lt;code&gt;SubscriptionCommandResponse&lt;/code&gt; indicating
success or failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code below is included as provided.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Subscription Service
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;SubscriptionService&lt;/code&gt; contains the business validation and prepares&lt;br&gt;
the subscription entity before saving it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Application.Common.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Application.ServicesContracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Domain.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Domain.Enums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Domain.RepositoryContracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;GymManagement.Application.Services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISubscriptionService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ISubscriptionRepository&lt;/span&gt; &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SubscriptionService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ISubscriptionRepository&lt;/span&gt; &lt;span class="n"&gt;subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SubscriptionCommandResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateSubscriptionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;gymId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;membershipPlanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PaymentMethod&lt;/span&gt; &lt;span class="n"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;termsAccepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid member."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gymId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid gym."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;membershipPlanId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid membership plan."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Validate the membership start date.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startDate&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"The membership start date cannot be in the past."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Validate the enum value.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentMethod&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid payment method."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Retrieve the selected membership plan.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMembershipPlanByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;membershipPlanId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Membership plan not found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Ensure the plan belongs to the selected gym.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GymId&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;gymId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"The selected membership plan does not belong to this gym."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Prevent more than one active membership in the same gym.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;alreadySubscribed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasActiveMembershipInGymAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;gymId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alreadySubscribed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"You cannot have more than one active membership at the same gym."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Limit the member to three active memberships across all gyms.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeMembershipCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountActiveMembershipsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activeMembershipCount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"You cannot have more than three active memberships across all gyms."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Validate the plan duration.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DurationInMonths&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"The membership plan duration must be greater than zero."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Calculate expiry using DateOnly.&lt;/span&gt;
        &lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;endDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DurationInMonths&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Unable to calculate the membership expiry date."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Set server-generated timestamps and status.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Subscription&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;MembershipPlanId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;StartDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;EndDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TermsAccepted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;termsAccepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SubscribedOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CreatedDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PricePaid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_subscriptionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSubscriptionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SubscriptionCommandResponse&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Membership subscription created successfully."&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SubscriptionCommandResponse&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SubscriptionCommandResponse&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="p"&gt;------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="err"&gt;##&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Subscription&lt;/span&gt; &lt;span class="n"&gt;Repository&lt;/span&gt;

&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;SubscriptionRepository&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="n"&gt;uses&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;Entity&lt;/span&gt; &lt;span class="n"&gt;Framework&lt;/span&gt;
&lt;span class="n"&gt;Core&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;membership&lt;/span&gt; &lt;span class="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 csharp&lt;br&gt;
using GymManagement.Domain.Entities;&lt;br&gt;
using GymManagement.Domain.RepositoryContracts;&lt;br&gt;
using GymManagement.Infrastructure.Context;&lt;br&gt;
using Microsoft.EntityFrameworkCore;&lt;/p&gt;

&lt;p&gt;namespace GymManagement.Infrastructure.Repository;&lt;/p&gt;

&lt;p&gt;public class SubscriptionRepository : ISubscriptionRepository&lt;br&gt;
{&lt;br&gt;
    private readonly AppDbContext _context;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public SubscriptionRepository(AppDbContext context)
{
    _context = context;
}

public async Task&amp;lt;MembershipPlan?&amp;gt; GetMembershipPlanByIdAsync(
    Guid planId,
    CancellationToken cancellationToken)
{
    return await _context.MembershipPlans
        .AsNoTracking()
        .FirstOrDefaultAsync(
            p =&amp;gt; p.Id == planId,
            cancellationToken);
}

public async Task&amp;lt;bool&amp;gt; HasActiveMembershipInGymAsync(
    Guid userId,
    Guid gymId,
    CancellationToken cancellationToken)
{
    var today = DateOnly.FromDateTime(DateTime.UtcNow);

    return await _context.Subscriptions
        .AnyAsync(s =&amp;gt;
            s.UserId == userId &amp;amp;&amp;amp;
            s.MembershipPlan != null &amp;amp;&amp;amp;
            s.MembershipPlan.GymId == gymId &amp;amp;&amp;amp;
            s.Status == "Active" &amp;amp;&amp;amp;
            s.EndDate &amp;gt;= today,
            cancellationToken);
}

public async Task&amp;lt;int&amp;gt; CountActiveMembershipsAsync(
    Guid userId,
    CancellationToken cancellationToken)
{
    var today = DateOnly.FromDateTime(DateTime.UtcNow);

    return await _context.Subscriptions
        .CountAsync(s =&amp;gt;
            s.UserId == userId &amp;amp;&amp;amp;
            s.Status == "Active" &amp;amp;&amp;amp;
            s.EndDate &amp;gt;= today,
            cancellationToken);
}

public async Task AddSubscriptionAsync(
    Subscription subscription,
    CancellationToken cancellationToken)
{
    await _context.Subscriptions.AddAsync(
        subscription,
        cancellationToken);

    await _context.SaveChangesAsync(cancellationToken);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building Interactive Blazor Pages with Reusable Components</title>
      <dc:creator>Lucia</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:55:50 +0000</pubDate>
      <link>https://dev.to/lucia_alethea_d/building-interactive-blazor-pages-with-reusable-components-k46</link>
      <guid>https://dev.to/lucia_alethea_d/building-interactive-blazor-pages-with-reusable-components-k46</guid>
      <description>&lt;h2&gt;
  
  
  A Small Blazor Demo: Updating a Counter
&lt;/h2&gt;

&lt;p&gt;Blazor lets a page respond to user actions without requiring a full browser-page reload.&lt;br&gt;
A Razor component usually combines HTML-like markup with C# logic.&lt;br&gt;
The markup defines the content that appears on the screen.&lt;br&gt;
The &lt;code&gt;@code&lt;/code&gt; block contains the state and methods used by that markup.&lt;br&gt;
In this example, a button updates a number displayed on the page.&lt;br&gt;
The &lt;code&gt;@page&lt;/code&gt; directive makes the component available at a browser route.&lt;br&gt;
The &lt;code&gt;@count&lt;/code&gt; expression displays the current value of the &lt;code&gt;count&lt;/code&gt; field.&lt;br&gt;
The &lt;code&gt;@onclick&lt;/code&gt; directive connects the button to a C# method.&lt;br&gt;
When the button is clicked, &lt;code&gt;IncrementCount&lt;/code&gt; changes the value.&lt;br&gt;
Blazor detects the event and renders the updated value.&lt;br&gt;
A second button demonstrates how another event can reset the same state.&lt;br&gt;
This small example introduces routing, component state, event handling, and rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@page "/counter-demo"

&amp;lt;PageTitle&amp;gt;Counter Demo&amp;lt;/PageTitle&amp;gt;

&amp;lt;div class="container py-4"&amp;gt;
    &amp;lt;h2&amp;gt;Counter Demo&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Current count: &amp;lt;strong&amp;gt;@count&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;button class="btn btn-primary me-2"
            @onclick="IncrementCount"&amp;gt;
        Increase
    &amp;lt;/button&amp;gt;

    &amp;lt;button class="btn btn-outline-secondary"
            @onclick="ResetCount"&amp;gt;
        Reset
    &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;

@code {
    private int count = 0;

    private void IncrementCount()
    {
        count++;
    }

    private void ResetCount()
    {
        count = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What the demo teaches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@page "/counter-demo"&lt;/code&gt; defines the route for the component.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;private int count = 0;&lt;/code&gt; stores the component's current state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@count&lt;/code&gt; renders that state in the markup.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@onclick="IncrementCount"&lt;/code&gt; connects a UI event to a C# method.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IncrementCount&lt;/code&gt; changes the state, and Blazor refreshes the rendered value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ResetCount&lt;/code&gt; demonstrates that multiple events can update the same state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following implementation applies these Blazor fundamentals to a larger interface, with separate pages and reusable components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Documentation: Pages and Reusable Components
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Purpose
&lt;/h2&gt;

&lt;p&gt;This implementation contains two routed pages and three reusable UI&lt;br&gt;
components. The page components own their API calls and page-level&lt;br&gt;
state. The child components focus on rendering reusable parts of the&lt;br&gt;
interface and raising events to their parent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Folder structure
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GymManagement.Client/
├── Pages/
│   ├── MyMemberships.razor
│   └── MyAttendance.razor
└── Components/
    └── Membership/
        ├── MembershipSummaryCards.razor
        ├── MembershipCard.razor
        └── MembershipCardList.razor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Component relationship
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;MyMemberships.razor&lt;/code&gt; is the parent page. It loads data, calls the
summary endpoint, handles searching and status filtering, sends
sorting parameters, controls pagination, and performs
cancellation/navigation actions.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;MembershipSummaryCards.razor&lt;/code&gt; receives four counts through
&lt;code&gt;[Parameter]&lt;/code&gt; properties and displays them.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;MembershipCardList.razor&lt;/code&gt; loops through the current page of records
and renders one &lt;code&gt;MembershipCard.razor&lt;/code&gt; for each record.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;MembershipCard.razor&lt;/code&gt; displays one record and uses
&lt;code&gt;EventCallback&amp;lt;Guid&amp;gt;&lt;/code&gt; to notify the list/page when the user chooses
Renew, Cancel, or Attendance.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;MyAttendance.razor&lt;/code&gt; loads attendance history and manages its own
pagination.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Important behavior
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Searching, filtering, sorting selections, and pagination state are
kept in &lt;code&gt;MyMemberships.razor&lt;/code&gt;; there are no separate search or
pagination components.&lt;/li&gt;
&lt;li&gt;  Attendance pagination is kept in &lt;code&gt;MyAttendance.razor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  DTOs and helper classes are declared inside the relevant Razor
files' &lt;code&gt;@code&lt;/code&gt; blocks; no separate &lt;code&gt;Models&lt;/code&gt; folder is used.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;SortBy&lt;/code&gt; and &lt;code&gt;SortDirection&lt;/code&gt; are sent to the API. The backend must
validate these values and sort the full filtered query before
applying pagination.&lt;/li&gt;
&lt;li&gt;  The summary cards use &lt;code&gt;api/Subscription/MyMembershipSummary&lt;/code&gt;, so the
backend should return counts across all records, not only the
current page.&lt;/li&gt;
&lt;li&gt;  The page expects the API response shapes shown in the code. Adjust
the inline DTOs if the backend uses different property names or
types.&lt;/li&gt;
&lt;li&gt;  The attendance DTO is illustrative and should be aligned with the
actual attendance endpoint response.&lt;/li&gt;
&lt;li&gt;  The API endpoints must enforce authentication and ownership on the
server. Do not trust a user ID supplied by the browser to decide
which records to return.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parent page: includes the summary component and list component, and&lt;br&gt;
keeps searching, filtering, sorting, pagination, API requests,&lt;br&gt;
navigation, and cancellation logic in its &lt;code&gt;@code&lt;/code&gt; block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@page "/my-memberships"

@using System.Net.Http.Json
@using System.Globalization
@using Microsoft.AspNetCore.Components.Forms
@using GymManagement.Client.Components.Membership
@using Blazor.SweetAlert2

@inject HttpClient Http
@inject NavigationManager Navigation
@inject SweetAlertService Swal

&amp;lt;PageTitle&amp;gt;My Memberships&amp;lt;/PageTitle&amp;gt;

&amp;lt;div class="container-fluid py-4"&amp;gt;

    &amp;lt;div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4"&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;h2 class="fw-bold mb-1"&amp;gt;
                &amp;lt;i class="bi bi-person-badge me-2"&amp;gt;&amp;lt;/i&amp;gt;
                My Memberships
            &amp;lt;/h2&amp;gt;
            &amp;lt;p class="text-secondary mb-0"&amp;gt;
                Manage your memberships and track their status.
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button class="btn btn-primary" @onclick="BrowseGyms"&amp;gt;
            &amp;lt;i class="bi bi-plus-circle me-1"&amp;gt;&amp;lt;/i&amp;gt;
            Browse Gyms
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;MembershipSummaryCards ActiveCount="@ActiveCount"
                            ExpiringCount="@ExpiringCount"
                            ExpiredCount="@ExpiredCount"
                            CancelledCount="@CancelledCount" /&amp;gt;

    @* Search, filter, and sorting are handled in this page. *@
    &amp;lt;div class="card border-0 shadow-sm mb-4"&amp;gt;
        &amp;lt;div class="card-body"&amp;gt;
            &amp;lt;EditForm Model="@SearchModel" OnValidSubmit="@SearchAsync"&amp;gt;
                &amp;lt;div class="row g-3 align-items-end"&amp;gt;

                    &amp;lt;div class="col-12 col-lg-4"&amp;gt;
                        &amp;lt;label class="form-label fw-semibold"&amp;gt;Search&amp;lt;/label&amp;gt;
                        &amp;lt;div class="input-group"&amp;gt;
                            &amp;lt;span class="input-group-text bg-white"&amp;gt;
                                &amp;lt;i class="bi bi-search"&amp;gt;&amp;lt;/i&amp;gt;
                            &amp;lt;/span&amp;gt;
                            &amp;lt;InputText class="form-control"
                                       @bind-Value="SearchModel.SearchTerm"
                                       placeholder="Gym or plan name..." /&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class="col-12 col-sm-6 col-lg-2"&amp;gt;
                        &amp;lt;label class="form-label fw-semibold"&amp;gt;Status&amp;lt;/label&amp;gt;
                        &amp;lt;InputSelect class="form-select"
                                      @bind-Value="SearchModel.Status"&amp;gt;
                            &amp;lt;option value=""&amp;gt;All statuses&amp;lt;/option&amp;gt;
                            &amp;lt;option value="Active"&amp;gt;Active&amp;lt;/option&amp;gt;
                            &amp;lt;option value="Expiring"&amp;gt;Expiring&amp;lt;/option&amp;gt;
                            &amp;lt;option value="Expired"&amp;gt;Expired&amp;lt;/option&amp;gt;
                            &amp;lt;option value="Cancelled"&amp;gt;Cancelled&amp;lt;/option&amp;gt;
                        &amp;lt;/InputSelect&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class="col-12 col-sm-6 col-lg-2"&amp;gt;
                        &amp;lt;label class="form-label fw-semibold"&amp;gt;Sort by&amp;lt;/label&amp;gt;
                        &amp;lt;InputSelect class="form-select"
                                     @bind-Value="SearchModel.SortBy"&amp;gt;
                            &amp;lt;option value="EndDate"&amp;gt;Expiry date&amp;lt;/option&amp;gt;
                            &amp;lt;option value="StartDate"&amp;gt;Start date&amp;lt;/option&amp;gt;
                            &amp;lt;option value="PlanName"&amp;gt;Plan name&amp;lt;/option&amp;gt;
                            &amp;lt;option value="PricePaid"&amp;gt;Price&amp;lt;/option&amp;gt;
                        &amp;lt;/InputSelect&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class="col-12 col-sm-6 col-lg-2"&amp;gt;
                        &amp;lt;label class="form-label fw-semibold"&amp;gt;Order&amp;lt;/label&amp;gt;
                        &amp;lt;InputSelect class="form-select"
                                     @bind-Value="SearchModel.SortDirection"&amp;gt;
                            &amp;lt;option value="Ascending"&amp;gt;Ascending&amp;lt;/option&amp;gt;
                            &amp;lt;option value="Descending"&amp;gt;Descending&amp;lt;/option&amp;gt;
                        &amp;lt;/InputSelect&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class="col-12 col-sm-6 col-lg-2 d-flex gap-2"&amp;gt;
                        &amp;lt;button type="submit"
                                class="btn btn-primary flex-grow-1"
                                disabled="@IsLoading"&amp;gt;
                            Search
                        &amp;lt;/button&amp;gt;

                        &amp;lt;button type="button"
                                class="btn btn-outline-secondary"
                                @onclick="ClearSearchAsync"
                                disabled="@IsLoading"&amp;gt;
                            Clear
                        &amp;lt;/button&amp;gt;
                    &amp;lt;/div&amp;gt;

                &amp;lt;/div&amp;gt;
            &amp;lt;/EditForm&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    @if (IsLoading)
    {
        &amp;lt;div class="text-center py-5"&amp;gt;
            &amp;lt;div class="spinner-border text-primary" role="status"&amp;gt;
                &amp;lt;span class="visually-hidden"&amp;gt;Loading...&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;p class="text-secondary mt-3"&amp;gt;Loading memberships...&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else if (!string.IsNullOrWhiteSpace(ErrorMessage))
    {
        &amp;lt;div class="alert alert-danger d-flex justify-content-between align-items-center"&amp;gt;
            &amp;lt;span&amp;gt;@ErrorMessage&amp;lt;/span&amp;gt;
            &amp;lt;button class="btn btn-outline-danger btn-sm"
                    @onclick="LoadPageAsync"&amp;gt;
                Retry
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else if (Memberships.Count == 0)
    {
        &amp;lt;div class="card border-0 shadow-sm"&amp;gt;
            &amp;lt;div class="card-body text-center py-5"&amp;gt;
                &amp;lt;i class="bi bi-card-list display-4 text-secondary"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;h5 class="mt-3"&amp;gt;No memberships found&amp;lt;/h5&amp;gt;
                &amp;lt;p class="text-secondary"&amp;gt;
                    Try another search or browse available gyms.
                &amp;lt;/p&amp;gt;
                &amp;lt;button class="btn btn-primary" @onclick="BrowseGyms"&amp;gt;
                    Browse Gyms
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else
    {
        &amp;lt;MembershipCardList Items="@Memberships"
                            ActionDisabled="@IsActionRunning"
                            OnRenew="@RenewMembership"
                            OnCancel="@CancelMembershipAsync"
                            OnAttendance="@ViewAttendance" /&amp;gt;

        @* Pagination is implemented here, not in a separate component. *@
        &amp;lt;div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mt-4"&amp;gt;

            &amp;lt;span class="text-secondary small"&amp;gt;
                Showing @FirstItem–@LastItem of @TotalCount memberships
            &amp;lt;/span&amp;gt;

            &amp;lt;div class="d-flex flex-wrap align-items-center gap-2"&amp;gt;
                &amp;lt;label for="membershipPageSize" class="small text-secondary"&amp;gt;
                    Per page
                &amp;lt;/label&amp;gt;

                &amp;lt;select id="membershipPageSize"
                        class="form-select form-select-sm"
                        style="width: 85px"
                        value="@PageSize"
                        @onchange="ChangePageSizeAsync"
                        disabled="@IsLoading"&amp;gt;
                    &amp;lt;option value="6"&amp;gt;6&amp;lt;/option&amp;gt;
                    &amp;lt;option value="9"&amp;gt;9&amp;lt;/option&amp;gt;
                    &amp;lt;option value="12"&amp;gt;12&amp;lt;/option&amp;gt;
                &amp;lt;/select&amp;gt;

                &amp;lt;button class="btn btn-outline-secondary btn-sm"
                        @onclick="PreviousPageAsync"
                        disabled="@(IsLoading || PageNumber &amp;lt;= 1)"&amp;gt;
                    &amp;lt;i class="bi bi-chevron-left"&amp;gt;&amp;lt;/i&amp;gt;
                    Previous
                &amp;lt;/button&amp;gt;

                &amp;lt;span class="small text-nowrap"&amp;gt;
                    Page @PageNumber of @TotalPages
                &amp;lt;/span&amp;gt;

                &amp;lt;button class="btn btn-outline-secondary btn-sm"
                        @onclick="NextPageAsync"
                        disabled="@(IsLoading || PageNumber &amp;gt;= TotalPages)"&amp;gt;
                    Next
                    &amp;lt;i class="bi bi-chevron-right"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    }

&amp;lt;/div&amp;gt;

@code {
    private List&amp;lt;MembershipCardList.MembershipItem&amp;gt; Memberships = new();

    private SearchModelType SearchModel = new();

    private bool IsLoading;
    private bool IsActionRunning;
    private string ErrorMessage = string.Empty;

    private int PageNumber = 1;
    private int PageSize = 6;
    private int TotalCount;
    private int TotalPages = 1;

    private int ActiveCount;
    private int ExpiringCount;
    private int ExpiredCount;
    private int CancelledCount;

    private int FirstItem =&amp;gt;
        TotalCount == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1;

    private int LastItem =&amp;gt;
        Math.Min(PageNumber * PageSize, TotalCount);

    protected override async Task OnInitializedAsync()
    {
        await LoadPageAsync();
    }

    private async Task LoadPageAsync()
    {
        IsLoading = true;
        ErrorMessage = string.Empty;

        try
        {
            var searchTerm = Uri.EscapeDataString(
                SearchModel.SearchTerm.Trim());

            var status = Uri.EscapeDataString(SearchModel.Status);
            var sortBy = Uri.EscapeDataString(SearchModel.SortBy);
            var sortDirection = Uri.EscapeDataString(
                SearchModel.SortDirection);

            var url =
                "api/Subscription/MyMemberships" +
                $"?SearchTerm={searchTerm}" +
                $"&amp;amp;Status={status}" +
                $"&amp;amp;SortBy={sortBy}" +
                $"&amp;amp;SortDirection={sortDirection}" +
                $"&amp;amp;PageNumber={PageNumber}" +
                $"&amp;amp;PageSize={PageSize}";

            var response =
                await Http.GetFromJsonAsync&amp;lt;
                    PagedResponse&amp;lt;MembershipResponseDto&amp;gt;&amp;gt;(url);

            if (response is null)
            {
                throw new InvalidOperationException(
                    "The server returned an empty memberships response.");
            }

            Memberships = response.Items.Select(m =&amp;gt;
            {
                var displayStatus = GetDisplayStatus(m);

                return new MembershipCardList.MembershipItem
                {
                    Id = m.Id,
                    GymId = m.GymId,
                    GymName = m.GymName,
                    MembershipPlanId = m.MembershipPlanId,
                    PlanName = m.PlanName,
                    StartDate = m.StartDate,
                    EndDate = m.EndDate,
                    PricePaid = m.PricePaid,
                    Status = m.Status,
                    DisplayStatus = displayStatus
                };
            }).ToList();

            PageNumber = response.PageNumber;
            PageSize = response.PageSize;
            TotalCount = response.TotalCount;
            TotalPages = Math.Max(1, response.TotalPages);

            // Get counts across ALL memberships from the summary endpoint.
            await LoadSummaryAsync();
        }
        catch (Exception ex)
        {
            ErrorMessage = "Unable to load memberships. " + ex.Message;
        }
        finally
        {
            IsLoading = false;
        }
    }

    private async Task LoadSummaryAsync()
    {
        var summary = await Http.GetFromJsonAsync&amp;lt;MembershipSummaryDto&amp;gt;(
            "api/Subscription/MyMembershipSummary");

        if (summary is null)
        {
            return;
        }

        ActiveCount = summary.ActiveCount;
        ExpiringCount = summary.ExpiringCount;
        ExpiredCount = summary.ExpiredCount;
        CancelledCount = summary.CancelledCount;
    }

    private async Task SearchAsync()
    {
        PageNumber = 1;
        await LoadPageAsync();
    }

    private async Task ClearSearchAsync()
    {
        SearchModel = new SearchModelType();
        PageNumber = 1;
        await LoadPageAsync();
    }

    private async Task PreviousPageAsync()
    {
        if (PageNumber &amp;gt; 1)
        {
            PageNumber--;
            await LoadPageAsync();
        }
    }

    private async Task NextPageAsync()
    {
        if (PageNumber &amp;lt; TotalPages)
        {
            PageNumber++;
            await LoadPageAsync();
        }
    }

    private async Task ChangePageSizeAsync(ChangeEventArgs args)
    {
        if (int.TryParse(args.Value?.ToString(), out var size) &amp;amp;&amp;amp;
            size is 6 or 9 or 12)
        {
            PageSize = size;
            PageNumber = 1;
            await LoadPageAsync();
        }
    }

    private static string GetDisplayStatus(MembershipResponseDto membership)
    {
        if (string.Equals(
            membership.Status, "Cancelled",
            StringComparison.OrdinalIgnoreCase))
        {
            return "Cancelled";
        }

        var today = DateOnly.FromDateTime(DateTime.UtcNow);

        if (membership.EndDate &amp;lt; today ||
            string.Equals(
                membership.Status, "Expired",
                StringComparison.OrdinalIgnoreCase))
        {
            return "Expired";
        }

        var daysRemaining = membership.EndDate.DayNumber - today.DayNumber;

        if (string.Equals(
                membership.Status, "Active",
                StringComparison.OrdinalIgnoreCase) &amp;amp;&amp;amp;
            daysRemaining is &amp;gt;= 0 and &amp;lt;= 7)
        {
            return "Expiring";
        }

        return membership.Status;
    }

    private void BrowseGyms()
    {
        Navigation.NavigateTo("/browse-gyms");
    }

    private void RenewMembership(Guid membershipId)
    {
        var membership = Memberships.FirstOrDefault(m =&amp;gt; m.Id == membershipId);

        if (membership is not null)
        {
            Navigation.NavigateTo(
                $"/subscribe?gymId={membership.GymId}" +
                $"&amp;amp;planId={membership.MembershipPlanId}");
        }
    }

    private void ViewAttendance(Guid membershipId)
    {
        Navigation.NavigateTo($"/my-attendance?membershipId={membershipId}");
    }

    private async Task CancelMembershipAsync(Guid membershipId)
    {
        var membership = Memberships.FirstOrDefault(m =&amp;gt; m.Id == membershipId);

        if (membership is null)
        {
            return;
        }

        var confirmation = await Swal.FireAsync(new SweetAlertOptions
        {
            Title = "Cancel membership?",
            Text = $"Are you sure you want to cancel {membership.PlanName}?",
            Icon = SweetAlertIcon.Warning,
            ShowCancelButton = true,
            ConfirmButtonText = "Yes, cancel",
            CancelButtonText = "Keep membership",
            ConfirmButtonColor = "#dc3545"
        });

        if (!confirmation.IsConfirmed)
        {
            return;
        }

        IsActionRunning = true;

        try
        {
            var response = await Http.PostAsync(
                $"api/Subscription/{membershipId}/Cancel",
                content: null);

            var result = await response.Content
                .ReadFromJsonAsync&amp;lt;SubscriptionCommandResponse&amp;gt;();

            if (response.IsSuccessStatusCode &amp;amp;&amp;amp; result?.Success == true)
            {
                await Swal.FireAsync(
                    "Cancelled",
                    result.Message,
                    SweetAlertIcon.Success);

                await LoadPageAsync();
            }
            else
            {
                await Swal.FireAsync(
                    "Unable to cancel",
                    result?.Message ?? "The membership could not be cancelled.",
                    SweetAlertIcon.Error);
            }
        }
        catch
        {
            await Swal.FireAsync(
                "Error",
                "An error occurred while cancelling the membership.",
                SweetAlertIcon.Error);
        }
        finally
        {
            IsActionRunning = false;
        }
    }

    private sealed class SearchModelType
    {
        public string SearchTerm { get; set; } = string.Empty;
        public string Status { get; set; } = string.Empty;
        public string SortBy { get; set; } = "EndDate";
        public string SortDirection { get; set; } = "Ascending";
    }

    private sealed class MembershipResponseDto
    {
        public Guid Id { get; set; }
        public Guid GymId { get; set; }
        public string GymName { get; set; } = string.Empty;
        public Guid MembershipPlanId { get; set; }
        public string PlanName { get; set; } = string.Empty;
        public DateOnly StartDate { get; set; }
        public DateOnly EndDate { get; set; }
        public decimal PricePaid { get; set; }
        public string Status { get; set; } = string.Empty;
    }

    private sealed class MembershipSummaryDto
    {
        public int ActiveCount { get; set; }
        public int ExpiringCount { get; set; }
        public int ExpiredCount { get; set; }
        public int CancelledCount { get; set; }
    }

    private sealed class PagedResponse&amp;lt;T&amp;gt;
    {
        public List&amp;lt;T&amp;gt; Items { get; set; } = new();
        public int PageNumber { get; set; }
        public int PageSize { get; set; }
        public int TotalCount { get; set; }
        public int TotalPages { get; set; }
    }

    private sealed class SubscriptionCommandResponse
    {
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pages/MyAttendance.razor
&lt;/h2&gt;

&lt;p&gt;Attendance page: reads an optional &lt;code&gt;membershipId&lt;/code&gt; from the query string,&lt;br&gt;
requests attendance records, and manages page navigation and page-size&lt;br&gt;
changes locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@page "/my-attendance"

@using System.Net.Http.Json
@using System.Globalization

@inject HttpClient Http
@inject NavigationManager Navigation

&amp;lt;PageTitle&amp;gt;My Attendance&amp;lt;/PageTitle&amp;gt;

&amp;lt;div class="container-fluid py-4"&amp;gt;

    &amp;lt;div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4"&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;h2 class="fw-bold mb-1"&amp;gt;
                &amp;lt;i class="bi bi-calendar-check me-2"&amp;gt;&amp;lt;/i&amp;gt;
                My Attendance
            &amp;lt;/h2&amp;gt;
            &amp;lt;p class="text-secondary mb-0"&amp;gt;
                Review your gym check-in and check-out history.
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button class="btn btn-outline-secondary" @onclick="BackToMemberships"&amp;gt;
            &amp;lt;i class="bi bi-arrow-left me-1"&amp;gt;&amp;lt;/i&amp;gt;
            My Memberships
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    @if (MembershipId.HasValue)
    {
        &amp;lt;div class="alert alert-info d-flex justify-content-between align-items-center"&amp;gt;
            &amp;lt;span&amp;gt;Showing attendance for the selected membership.&amp;lt;/span&amp;gt;
            &amp;lt;button class="btn btn-sm btn-outline-primary"
                    @onclick="ShowAllAttendanceAsync"&amp;gt;
                Show all
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    }

    @if (IsLoading)
    {
        &amp;lt;div class="text-center py-5"&amp;gt;
            &amp;lt;div class="spinner-border text-primary" role="status"&amp;gt;
                &amp;lt;span class="visually-hidden"&amp;gt;Loading...&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;p class="text-secondary mt-3"&amp;gt;Loading attendance...&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else if (!string.IsNullOrWhiteSpace(ErrorMessage))
    {
        &amp;lt;div class="alert alert-danger d-flex justify-content-between align-items-center"&amp;gt;
            &amp;lt;span&amp;gt;@ErrorMessage&amp;lt;/span&amp;gt;
            &amp;lt;button class="btn btn-outline-danger btn-sm" @onclick="LoadAttendanceAsync"&amp;gt;
                Retry
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else if (AttendanceRecords.Count == 0)
    {
        &amp;lt;div class="card border-0 shadow-sm"&amp;gt;
            &amp;lt;div class="card-body text-center py-5"&amp;gt;
                &amp;lt;i class="bi bi-calendar-x display-4 text-secondary"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;h5 class="mt-3"&amp;gt;No attendance records found&amp;lt;/h5&amp;gt;
                &amp;lt;p class="text-secondary mb-0"&amp;gt;
                    Your attendance records will appear here when available.
                &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    else
    {
        &amp;lt;div class="card border-0 shadow-sm"&amp;gt;
            &amp;lt;div class="card-body p-0"&amp;gt;
                &amp;lt;div class="table-responsive"&amp;gt;
                    &amp;lt;table class="table table-hover align-middle mb-0"&amp;gt;
                        &amp;lt;thead class="table-light"&amp;gt;
                            &amp;lt;tr&amp;gt;
                                &amp;lt;th class="px-3"&amp;gt;Date&amp;lt;/th&amp;gt;
                                &amp;lt;th&amp;gt;Gym&amp;lt;/th&amp;gt;
                                &amp;lt;th&amp;gt;Membership Plan&amp;lt;/th&amp;gt;
                                &amp;lt;th&amp;gt;Check-in&amp;lt;/th&amp;gt;
                                &amp;lt;th&amp;gt;Check-out&amp;lt;/th&amp;gt;
                                &amp;lt;th&amp;gt;Status&amp;lt;/th&amp;gt;
                            &amp;lt;/tr&amp;gt;
                        &amp;lt;/thead&amp;gt;

                        &amp;lt;tbody&amp;gt;
                            @foreach (var record in AttendanceRecords)
                            {
                                &amp;lt;tr @key="record.Id"&amp;gt;
                                    &amp;lt;td class="px-3 text-nowrap"&amp;gt;
                                        @record.CheckInTime.ToLocalTime().ToString("dd MMM yyyy")
                                    &amp;lt;/td&amp;gt;

                                    &amp;lt;td&amp;gt;@record.GymName&amp;lt;/td&amp;gt;

                                    &amp;lt;td&amp;gt;@record.PlanName&amp;lt;/td&amp;gt;

                                    &amp;lt;td class="text-nowrap"&amp;gt;
                                        @record.CheckInTime.ToLocalTime().ToString("hh:mm tt")
                                    &amp;lt;/td&amp;gt;

                                    &amp;lt;td class="text-nowrap"&amp;gt;
                                        @if (record.CheckOutTime.HasValue)
                                        {
                                            @record.CheckOutTime.Value.ToLocalTime().ToString("hh:mm tt")
                                        }
                                        else
                                        {
                                            &amp;lt;span class="text-secondary"&amp;gt;Not checked out&amp;lt;/span&amp;gt;
                                        }
                                    &amp;lt;/td&amp;gt;

                                    &amp;lt;td&amp;gt;
                                        &amp;lt;span class="@GetAttendanceBadgeClass(record.Status)"&amp;gt;
                                            @record.Status
                                        &amp;lt;/span&amp;gt;
                                    &amp;lt;/td&amp;gt;
                                &amp;lt;/tr&amp;gt;
                            }
                        &amp;lt;/tbody&amp;gt;
                    &amp;lt;/table&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        @* Pagination stays inside this page. *@
        &amp;lt;div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mt-4"&amp;gt;

            &amp;lt;span class="text-secondary small"&amp;gt;
                Showing @FirstItem–@LastItem of @TotalCount records
            &amp;lt;/span&amp;gt;

            &amp;lt;div class="d-flex flex-wrap align-items-center gap-2"&amp;gt;
                &amp;lt;label for="attendancePageSize" class="small text-secondary"&amp;gt;
                    Per page
                &amp;lt;/label&amp;gt;

                &amp;lt;select id="attendancePageSize"
                        class="form-select form-select-sm"
                        style="width: 85px"
                        value="@PageSize"
                        @onchange="ChangePageSizeAsync"
                        disabled="@IsLoading"&amp;gt;
                    &amp;lt;option value="6"&amp;gt;6&amp;lt;/option&amp;gt;
                    &amp;lt;option value="9"&amp;gt;9&amp;lt;/option&amp;gt;
                    &amp;lt;option value="12"&amp;gt;12&amp;lt;/option&amp;gt;
                &amp;lt;/select&amp;gt;

                &amp;lt;button class="btn btn-outline-secondary btn-sm"
                        @onclick="PreviousPageAsync"
                        disabled="@(IsLoading || PageNumber &amp;lt;= 1)"&amp;gt;
                    &amp;lt;i class="bi bi-chevron-left"&amp;gt;&amp;lt;/i&amp;gt;
                    Previous
                &amp;lt;/button&amp;gt;

                &amp;lt;span class="small text-nowrap"&amp;gt;
                    Page @PageNumber of @TotalPages
                &amp;lt;/span&amp;gt;

                &amp;lt;button class="btn btn-outline-secondary btn-sm"
                        @onclick="NextPageAsync"
                        disabled="@(IsLoading || PageNumber &amp;gt;= TotalPages)"&amp;gt;
                    Next
                    &amp;lt;i class="bi bi-chevron-right"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    }

&amp;lt;/div&amp;gt;

@code {
    [SupplyParameterFromQuery(Name = "membershipId")]
    public Guid? MembershipId { get; set; }

    private List&amp;lt;AttendanceItem&amp;gt; AttendanceRecords = new();

    private bool IsLoading;
    private string ErrorMessage = string.Empty;

    private int PageNumber = 1;
    private int PageSize = 6;
    private int TotalCount;
    private int TotalPages = 1;

    private int FirstItem =&amp;gt;
        TotalCount == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1;

    private int LastItem =&amp;gt;
        Math.Min(PageNumber * PageSize, TotalCount);

    protected override async Task OnParametersSetAsync()
    {
        PageNumber = 1;
        await LoadAttendanceAsync();
    }

    private async Task LoadAttendanceAsync()
    {
        IsLoading = true;
        ErrorMessage = string.Empty;

        try
        {
            var url =
                $"api/Attendance/MyAttendance" +
                $"?PageNumber={PageNumber}" +
                $"&amp;amp;PageSize={PageSize}";

            if (MembershipId.HasValue)
            {
                url += $"&amp;amp;MembershipId={MembershipId.Value}";
            }

            var response =
                await Http.GetFromJsonAsync&amp;lt;PagedResponse&amp;lt;AttendanceItem&amp;gt;&amp;gt;(url);

            if (response is null)
            {
                throw new InvalidOperationException(
                    "The server returned an empty attendance response.");
            }

            AttendanceRecords = response.Items ?? new List&amp;lt;AttendanceItem&amp;gt;();
            PageNumber = response.PageNumber;
            PageSize = response.PageSize;
            TotalCount = response.TotalCount;
            TotalPages = Math.Max(1, response.TotalPages);
        }
        catch (Exception ex)
        {
            ErrorMessage = "Unable to load attendance. " + ex.Message;
        }
        finally
        {
            IsLoading = false;
        }
    }

    private async Task PreviousPageAsync()
    {
        if (PageNumber &amp;gt; 1)
        {
            PageNumber--;
            await LoadAttendanceAsync();
        }
    }

    private async Task NextPageAsync()
    {
        if (PageNumber &amp;lt; TotalPages)
        {
            PageNumber++;
            await LoadAttendanceAsync();
        }
    }

    private async Task ChangePageSizeAsync(ChangeEventArgs args)
    {
        if (int.TryParse(args.Value?.ToString(), out var size) &amp;amp;&amp;amp;
            size is 6 or 9 or 12)
        {
            PageSize = size;
            PageNumber = 1;
            await LoadAttendanceAsync();
        }
    }

    private async Task ShowAllAttendanceAsync()
    {
        Navigation.NavigateTo("/my-attendance");
        MembershipId = null;
        PageNumber = 1;
        await LoadAttendanceAsync();
    }

    private void BackToMemberships()
    {
        Navigation.NavigateTo("/my-memberships");
    }

    private static string GetAttendanceBadgeClass(string status)
    {
        return status.ToLowerInvariant() switch
        {
            "present" or "completed" or "checked out" =&amp;gt;
                "badge text-bg-success",

            "checked in" =&amp;gt;
                "badge text-bg-primary",

            "missed" or "absent" =&amp;gt;
                "badge text-bg-danger",

            "cancelled" =&amp;gt;
                "badge text-bg-secondary",

            _ =&amp;gt;
                "badge text-bg-light"
        };
    }

    // Adjust these properties to match your actual Attendance entity/API DTO.
    private sealed class AttendanceItem
    {
        public Guid Id { get; set; }
        public string GymName { get; set; } = string.Empty;
        public string PlanName { get; set; } = string.Empty;
        public DateTime CheckInTime { get; set; }
        public DateTime? CheckOutTime { get; set; }
        public string Status { get; set; } = string.Empty;
    }

    private sealed class PagedResponse&amp;lt;T&amp;gt;
    {
        public List&amp;lt;T&amp;gt; Items { get; set; } = new();
        public int PageNumber { get; set; }
        public int PageSize { get; set; }
        public int TotalCount { get; set; }
        public int TotalPages { get; set; }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Components/Membership/MembershipSummaryCards.razor
&lt;/h2&gt;

&lt;p&gt;Reusable summary component: accepts four count parameters and displays&lt;br&gt;
them as responsive cards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="row g-3 mb-4"&amp;gt;
    &amp;lt;div class="col-12 col-sm-6 col-xl-3"&amp;gt;
        &amp;lt;div class="card border-0 shadow-sm h-100"&amp;gt;
            &amp;lt;div class="card-body d-flex justify-content-between align-items-center"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;p class="text-secondary mb-1"&amp;gt;Active&amp;lt;/p&amp;gt;
                    &amp;lt;h3 class="fw-bold mb-0"&amp;gt;@ActiveCount&amp;lt;/h3&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="summary-icon bg-success-subtle text-success"&amp;gt;
                    &amp;lt;i class="bi bi-check-circle fs-3"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="col-12 col-sm-6 col-xl-3"&amp;gt;
        &amp;lt;div class="card border-0 shadow-sm h-100"&amp;gt;
            &amp;lt;div class="card-body d-flex justify-content-between align-items-center"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;p class="text-secondary mb-1"&amp;gt;Expiring Soon&amp;lt;/p&amp;gt;
                    &amp;lt;h3 class="fw-bold mb-0"&amp;gt;@ExpiringCount&amp;lt;/h3&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="summary-icon bg-warning-subtle text-warning"&amp;gt;
                    &amp;lt;i class="bi bi-hourglass-split fs-3"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="col-12 col-sm-6 col-xl-3"&amp;gt;
        &amp;lt;div class="card border-0 shadow-sm h-100"&amp;gt;
            &amp;lt;div class="card-body d-flex justify-content-between align-items-center"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;p class="text-secondary mb-1"&amp;gt;Expired&amp;lt;/p&amp;gt;
                    &amp;lt;h3 class="fw-bold mb-0"&amp;gt;@ExpiredCount&amp;lt;/h3&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="summary-icon bg-danger-subtle text-danger"&amp;gt;
                    &amp;lt;i class="bi bi-calendar-x fs-3"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="col-12 col-sm-6 col-xl-3"&amp;gt;
        &amp;lt;div class="card border-0 shadow-sm h-100"&amp;gt;
            &amp;lt;div class="card-body d-flex justify-content-between align-items-center"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;p class="text-secondary mb-1"&amp;gt;Cancelled&amp;lt;/p&amp;gt;
                    &amp;lt;h3 class="fw-bold mb-0"&amp;gt;@CancelledCount&amp;lt;/h3&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class="summary-icon bg-secondary-subtle text-secondary"&amp;gt;
                    &amp;lt;i class="bi bi-x-circle fs-3"&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
    .summary-icon {
        width: 52px;
        height: 52px;
        border-radius: 14px;
        display: flex;
        align-items: center;
        justify-content: center;
    }
&amp;lt;/style&amp;gt;

@code {
    [Parameter] public int ActiveCount { get; set; }
    [Parameter] public int ExpiringCount { get; set; }
    [Parameter] public int ExpiredCount { get; set; }
    [Parameter] public int CancelledCount { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Components/Membership/MembershipCard.razor
&lt;/h2&gt;

&lt;p&gt;Reusable single-record component: displays dates, price, status, and&lt;br&gt;
action buttons. It reports button actions using &lt;code&gt;EventCallback&amp;lt;Guid&amp;gt;&lt;/code&gt;&lt;br&gt;
rather than making API calls itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="col-12 col-md-6 col-xl-4"&amp;gt;
    &amp;lt;div class="card membership-card border-0 shadow-sm h-100"&amp;gt;
        &amp;lt;div class="card-body p-4 d-flex flex-column"&amp;gt;

            &amp;lt;div class="d-flex justify-content-between align-items-start gap-2 mb-3"&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;h5 class="fw-bold mb-1"&amp;gt;@PlanName&amp;lt;/h5&amp;gt;
                    &amp;lt;p class="text-secondary mb-0"&amp;gt;
                        &amp;lt;i class="bi bi-building me-1"&amp;gt;&amp;lt;/i&amp;gt;
                        @GymName
                    &amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;span class="@GetBadgeClass(Status)"&amp;gt;@Status&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;hr /&amp;gt;

            &amp;lt;div class="row g-3 mb-3"&amp;gt;
                &amp;lt;div class="col-6"&amp;gt;
                    &amp;lt;small class="text-secondary d-block"&amp;gt;Start Date&amp;lt;/small&amp;gt;
                    &amp;lt;span class="fw-semibold"&amp;gt;@StartDate.ToString("dd MMM yyyy")&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="col-6"&amp;gt;
                    &amp;lt;small class="text-secondary d-block"&amp;gt;Expiry Date&amp;lt;/small&amp;gt;
                    &amp;lt;span class="fw-semibold"&amp;gt;@EndDate.ToString("dd MMM yyyy")&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="col-6"&amp;gt;
                    &amp;lt;small class="text-secondary d-block"&amp;gt;Amount Paid&amp;lt;/small&amp;gt;
                    &amp;lt;span class="fw-bold text-primary"&amp;gt;
                        @PricePaid.ToString("C", CultureInfo.GetCultureInfo("en-IN"))
                    &amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="col-6"&amp;gt;
                    &amp;lt;small class="text-secondary d-block"&amp;gt;Membership ID&amp;lt;/small&amp;gt;
                    &amp;lt;span class="small text-break"&amp;gt;@MembershipId.ToString("N")[..8]&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            @if (Status == "Expiring")
            {
                var daysLeft = EndDate.DayNumber -
                    DateOnly.FromDateTime(DateTime.UtcNow).DayNumber;

                &amp;lt;div class="alert alert-warning py-2 small"&amp;gt;
                    &amp;lt;i class="bi bi-clock me-1"&amp;gt;&amp;lt;/i&amp;gt;
                    @if (daysLeft &amp;lt;= 0)
                    {
                        &amp;lt;span&amp;gt;Membership expires today.&amp;lt;/span&amp;gt;
                    }
                    else
                    {
                        &amp;lt;span&amp;gt;Membership expires in @daysLeft day(s).&amp;lt;/span&amp;gt;
                    }
                &amp;lt;/div&amp;gt;
            }

            &amp;lt;div class="d-flex flex-wrap gap-2 mt-auto pt-2"&amp;gt;
                @if (Status != "Cancelled")
                {
                    &amp;lt;button class="btn btn-primary flex-grow-1"
                            @onclick="OnRenewClicked"
                            disabled="@ActionDisabled"&amp;gt;
                        &amp;lt;i class="bi bi-arrow-repeat me-1"&amp;gt;&amp;lt;/i&amp;gt;
                        Renew
                    &amp;lt;/button&amp;gt;
                }

                @if (Status == "Active" || Status == "Expiring")
                {
                    &amp;lt;button class="btn btn-outline-danger flex-grow-1"
                            @onclick="OnCancelClicked"
                            disabled="@ActionDisabled"&amp;gt;
                        &amp;lt;i class="bi bi-x-circle me-1"&amp;gt;&amp;lt;/i&amp;gt;
                        Cancel
                    &amp;lt;/button&amp;gt;
                }

                &amp;lt;button class="btn btn-outline-secondary"
                        @onclick="OnAttendanceClicked"&amp;gt;
                    &amp;lt;i class="bi bi-calendar-check me-1"&amp;gt;&amp;lt;/i&amp;gt;
                    Attendance
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style&amp;gt;
    .membership-card {
        border-radius: 16px;
        transition: transform .2s ease, box-shadow .2s ease;
    }

    .membership-card:hover {
        transform: translateY(-3px);
        box-shadow: 0 .75rem 1.5rem rgba(0, 0, 0, .10) !important;
    }
&amp;lt;/style&amp;gt;

@code {
    [Parameter] public Guid MembershipId { get; set; }
    [Parameter] public Guid GymId { get; set; }
    [Parameter] public Guid MembershipPlanId { get; set; }

    [Parameter] public string GymName { get; set; } = string.Empty;
    [Parameter] public string PlanName { get; set; } = string.Empty;
    [Parameter] public string Status { get; set; } = string.Empty;

    [Parameter] public DateOnly StartDate { get; set; }
    [Parameter] public DateOnly EndDate { get; set; }
    [Parameter] public decimal PricePaid { get; set; }

    [Parameter] public bool ActionDisabled { get; set; }

    [Parameter] public EventCallback&amp;lt;Guid&amp;gt; OnRenew { get; set; }
    [Parameter] public EventCallback&amp;lt;Guid&amp;gt; OnCancel { get; set; }
    [Parameter] public EventCallback&amp;lt;Guid&amp;gt; OnAttendance { get; set; }

    private string GetBadgeClass(string status)
    {
        return status switch
        {
            "Active" =&amp;gt; "badge rounded-pill text-bg-success",
            "Expiring" =&amp;gt; "badge rounded-pill text-bg-warning",
            "Expired" =&amp;gt; "badge rounded-pill text-bg-danger",
            "Cancelled" =&amp;gt; "badge rounded-pill text-bg-secondary",
            _ =&amp;gt; "badge rounded-pill text-bg-light"
        };
    }

    private Task OnRenewClicked() =&amp;gt;
        OnRenew.InvokeAsync(MembershipId);

    private Task OnCancelClicked() =&amp;gt;
        OnCancel.InvokeAsync(MembershipId);

    private Task OnAttendanceClicked() =&amp;gt;
        OnAttendance.InvokeAsync(MembershipId);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Components/Membership/MembershipCardList.razor
&lt;/h2&gt;

&lt;p&gt;List component: loops over the items and renders &lt;code&gt;MembershipCard&lt;/code&gt;. It&lt;br&gt;
forwards the action callbacks to the parent page and declares the list&lt;br&gt;
item class in its own &lt;code&gt;@code&lt;/code&gt; block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="row g-4"&amp;gt;
    @foreach (var membership in Items)
    {
        &amp;lt;MembershipCard @key="membership.Id"
                        MembershipId="@membership.Id"
                        GymId="@membership.GymId"
                        MembershipPlanId="@membership.MembershipPlanId"
                        GymName="@membership.GymName"
                        PlanName="@membership.PlanName"
                        StartDate="@membership.StartDate"
                        EndDate="@membership.EndDate"
                        PricePaid="@membership.PricePaid"
                        Status="@membership.DisplayStatus"
                        ActionDisabled="@ActionDisabled"
                        OnRenew="@OnRenew"
                        OnCancel="@OnCancel"
                        OnAttendance="@OnAttendance" /&amp;gt;
    }
&amp;lt;/div&amp;gt;

@code {
    [Parameter]
    public List&amp;lt;MembershipItem&amp;gt; Items { get; set; } = new();

    [Parameter]
    public bool ActionDisabled { get; set; }

    [Parameter]
    public EventCallback&amp;lt;Guid&amp;gt; OnRenew { get; set; }

    [Parameter]
    public EventCallback&amp;lt;Guid&amp;gt; OnCancel { get; set; }

    [Parameter]
    public EventCallback&amp;lt;Guid&amp;gt; OnAttendance { get; set; }

    public sealed class MembershipItem
    {
        public Guid Id { get; set; }
        public Guid GymId { get; set; }
        public string GymName { get; set; } = string.Empty;

        public Guid MembershipPlanId { get; set; }
        public string PlanName { get; set; } = string.Empty;

        public DateOnly StartDate { get; set; }
        public DateOnly EndDate { get; set; }

        public decimal PricePaid { get; set; }
        public string Status { get; set; } = string.Empty;

        // Used for displaying Expiring when an active membership
        // has seven or fewer days remaining.
        public string DisplayStatus { get; set; } = string.Empty;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup and verification checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Confirm the namespace in
&lt;code&gt;@using GymManagement.Client.Components.Membership&lt;/code&gt; matches the
actual client project namespace.&lt;/li&gt;
&lt;li&gt;  Ensure Bootstrap CSS and Bootstrap Icons are loaded by the
application.&lt;/li&gt;
&lt;li&gt;  Ensure the SweetAlert2 package and its service registration are
configured, because the main page injects &lt;code&gt;SweetAlertService&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Ensure &lt;code&gt;HttpClient&lt;/code&gt; is configured with the correct backend base
address and authentication handler, if the API requires JWT
authentication.&lt;/li&gt;
&lt;li&gt;  Confirm that &lt;code&gt;DateOnly&lt;/code&gt; values serialize and deserialize in the
format expected by the backend.&lt;/li&gt;
&lt;li&gt;  Confirm the backend supports the query parameters &lt;code&gt;SearchTerm&lt;/code&gt;,
&lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;SortBy&lt;/code&gt;, &lt;code&gt;SortDirection&lt;/code&gt;, &lt;code&gt;PageNumber&lt;/code&gt;, and &lt;code&gt;PageSize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Confirm the summary endpoint returns &lt;code&gt;ActiveCount&lt;/code&gt;, &lt;code&gt;ExpiringCount&lt;/code&gt;,
&lt;code&gt;ExpiredCount&lt;/code&gt;, and &lt;code&gt;CancelledCount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Confirm cancellation returns a JSON object with &lt;code&gt;Success&lt;/code&gt; and
&lt;code&gt;Message&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Confirm attendance returns &lt;code&gt;Items&lt;/code&gt;, &lt;code&gt;PageNumber&lt;/code&gt;, &lt;code&gt;PageSize&lt;/code&gt;,
&lt;code&gt;TotalCount&lt;/code&gt;, and &lt;code&gt;TotalPages&lt;/code&gt;, and each item contains the
properties declared in &lt;code&gt;AttendanceItem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Test empty results, failed API requests, page-size changes,
next/previous navigation, status filtering, sorting in both
directions, cancellation confirmation, and navigation to the
attendance page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A note about sorting and pagination
&lt;/h2&gt;

&lt;p&gt;The frontend passes sorting and pagination choices to the backend. For&lt;br&gt;
the results to be correct across the complete dataset, the backend&lt;br&gt;
should apply search and status filtering first, then apply an&lt;br&gt;
allowlisted sort field and direction, then calculate the total count,&lt;br&gt;
and finally apply &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt; (or equivalent pagination). Sorting only&lt;br&gt;
the records already loaded on the current page will not sort the entire&lt;br&gt;
dataset.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
