DEV Community

ayush
ayush

Posted on

blazor component

Manage Gyms

@page "/admin/gyms"

@using System.Net.Http.Json
@using System.Net.Http
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@using Ayush_Assignment_11.Components.Admin

@attribute [Authorize(Roles = "Admin")]

@inject HttpClient Http
@inject IJSRuntime JS

Manage Gyms

Manage Gyms

Add, edit, search, and manage gym information.

Add Gym @if (!string.IsNullOrWhiteSpace(errorMessage)) { @errorMessage } Search gyms Sort by Gym name City Status Refresh @if (isLoading) { Loading...

Loading gyms...

} else if (FilteredGyms.Count == 0) {
No gyms found

Try another search or add a new gym.

} else {
All Gyms
@FilteredGyms.Count result(s) @foreach (var gym in FilteredGyms) { } }

@if (showForm)
{


role="dialog" aria-modal="true" aria-labelledby="gymFormTitle"
@onclick:stopPropagation="true">


@(isEditMode ? "Edit Gym" : "Add Gym")

@onclick="CloseForm" disabled="@isSaving">







Gym name


City


Address


Phone


Email


Opening time



Closing time



Total class capacity


Status
disabled="@(!isEditMode)">
Active
Inactive





Locker facility available





Parking available




@onclick="CloseForm" disabled="@isSaving">Cancel

@if (isSaving)
{

}
@(isEditMode ? "Update Gym" : "Save Gym")






}

@code {

public class GymCardModel
{
    public Guid Id { get; set; }
    public string GymName { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string CityName { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public TimeSpan OpeningTime { get; set; }
    public TimeSpan ClosingTime { get; set; }
    public int TotalClassCapacity { get; set; }
    public bool LockerFacility { get; set; }
    public bool ParkingAvailable { get; set; }
    public string Status { get; set; } = "Active";
}

public class GymFormModel
{
    public Guid Id { get; set; }

    [Required(ErrorMessage = "Gym name is required.")]
    public string GymName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Address is required.")]
    public string Address { get; set; } = string.Empty;

    [Required(ErrorMessage = "City is required.")]
    public string CityName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Phone is required.")]
    public string Phone { get; set; } = string.Empty;

    [Required(ErrorMessage = "Email is required.")]
    [EmailAddress(ErrorMessage = "Enter a valid email address.")]
    public string Email { get; set; } = string.Empty;

    public string OpeningTime { get; set; } = "06:00";
    public string ClosingTime { get; set; } = "22:00";

    [Range(1, int.MaxValue, ErrorMessage = "Class capacity must be greater than zero.")]
    public int TotalClassCapacity { get; set; } = 20;

    public bool LockerFacility { get; set; }
    public bool ParkingAvailable { get; set; }
    public string Status { get; set; } = "Active";
}

public class OperationResponse
{
    public bool Success { get; set; }
    public string Message { get; set; } = string.Empty;
}

private List<GymCardModel> gyms = new();
private GymFormModel formModel = new();
private bool isLoading;
private bool isSaving;
private bool showForm;
private bool isEditMode;
private string searchText = string.Empty;
private string sortBy = "name";
private string errorMessage = string.Empty;

private List<GymCardModel> FilteredGyms =>
    gyms
        .Where(g =>
            string.IsNullOrWhiteSpace(searchText) ||
            g.GymName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
            g.CityName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
            g.Phone.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
            g.Email.Contains(searchText, StringComparison.OrdinalIgnoreCase))
        .OrderBy(g => sortBy switch
        {
            "city" => g.CityName,
            "status" => g.Status,
            _ => g.GymName
        })
        .ToList();

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

private async Task LoadGymsAsync()
{
    isLoading = true;
    errorMessage = string.Empty;
    try
    {
        var result = await Http.GetFromJsonAsync<List<GymCardModel>>("api/GymCenter");
        gyms = result ?? new List<GymCardModel>();
    }
    catch (Exception)
    {
        errorMessage = "Unable to load gyms. Check your API connection and endpoint.";
    }
    finally
    {
        isLoading = false;
    }
}

private void OpenAddForm()
{
    formModel = new GymFormModel
    {
        OpeningTime = "06:00",
        ClosingTime = "22:00",
        TotalClassCapacity = 20,
        Status = "Active"
    };
    isEditMode = false;
    showForm = true;
}

private async Task OpenEditForm(Guid id)
{
    var gym = gyms.FirstOrDefault(g => g.Id == id);
    if (gym is null)
    {
        await ShowMessageAsync("Error", "Gym not found.", false);
        return;
    }

    formModel = new GymFormModel
    {
        Id = gym.Id,
        GymName = gym.GymName,
        Address = gym.Address,
        CityName = gym.CityName,
        Phone = gym.Phone,
        Email = gym.Email,
        OpeningTime = gym.OpeningTime.ToString(@"hh\:mm"),
        ClosingTime = gym.ClosingTime.ToString(@"hh\:mm"),
        TotalClassCapacity = gym.TotalClassCapacity,
        LockerFacility = gym.LockerFacility,
        ParkingAvailable = gym.ParkingAvailable,
        Status = gym.Status
    };
    isEditMode = true;
    showForm = true;
}

private async Task SaveGymAsync()
{
    if (!TimeSpan.TryParse(formModel.OpeningTime, out var openingTime) ||
        !TimeSpan.TryParse(formModel.ClosingTime, out var closingTime))
    {
        await ShowMessageAsync("Validation error",
            "Please enter valid opening and closing times.", false);
        return;
    }

    if (closingTime <= openingTime)
    {
        await ShowMessageAsync("Validation error",
            "Closing time must be later than opening time.", false);
        return;
    }

    isSaving = true;
    try
    {
        HttpResponseMessage response;
        if (isEditMode)
        {
            var requestBody = new
            {
                Id = formModel.Id,
                formModel.GymName,
                formModel.Address,
                formModel.CityName,
                formModel.Phone,
                formModel.Email,
                OpeningTime = openingTime,
                ClosingTime = closingTime,
                formModel.TotalClassCapacity,
                formModel.LockerFacility,
                formModel.ParkingAvailable,
                formModel.Status
            };
            response = await Http.PutAsJsonAsync($"api/GymCenter/{formModel.Id}", requestBody);
        }
        else
        {
            var requestBody = new
            {
                formModel.GymName,
                formModel.Address,
                formModel.CityName,
                formModel.Phone,
                formModel.Email,
                OpeningTime = openingTime,
                ClosingTime = closingTime,
                formModel.TotalClassCapacity,
                formModel.LockerFacility,
                formModel.ParkingAvailable
            };
            response = await Http.PostAsJsonAsync("api/GymCenter", requestBody);
        }

        var result = await response.Content.ReadFromJsonAsync<OperationResponse>();
        if (!response.IsSuccessStatusCode || result?.Success != true)
        {
            await ShowMessageAsync("Operation failed",
                result?.Message ?? "The request could not be completed.", false);
            return;
        }

        showForm = false;
        await LoadGymsAsync();
        await ShowMessageAsync("Success", result.Message, true);
    }
    catch (Exception)
    {
        await ShowMessageAsync("Error", "An error occurred while saving the gym.", false);
    }
    finally
    {
        isSaving = false;
    }
}

private async Task DeleteGymAsync(Guid id)
{
    var confirmed = await JS.InvokeAsync<bool>(
        "gymUi.confirmDelete", "Are you sure?", "This record will be soft-deleted.");

    if (!confirmed)
        return;

    try
    {
        var response = await Http.DeleteAsync($"api/GymCenter/{id}");
        var result = await response.Content.ReadFromJsonAsync<OperationResponse>();

        if (!response.IsSuccessStatusCode || result?.Success != true)
        {
            await ShowMessageAsync("Unable to delete",
                result?.Message ?? "The deletion request failed.", false);
            return;
        }

        await LoadGymsAsync();
        await ShowMessageAsync("Success", result.Message, true);
    }
    catch (Exception)
    {
        await ShowMessageAsync("Error", "An error occurred while deleting the gym.", false);
    }
}

private void CloseForm()
{
    if (!isSaving)
        showForm = false;
}

private async Task ShowMessageAsync(string title, string message, bool success)
{
    await JS.InvokeVoidAsync("gymUi.showMessage", title, message, success);
}

}




\### Explanation of ManageGyms.razor



\-   `\[Authorize(Roles = "Admin")]` restricts the page to users in the

&#x20;   Admin role, assuming authorization is configured.

\-   `OnInitializedAsync()` loads the gyms when the component

&#x20;   initializes.

\-   `FilteredGyms` filters by name, city, phone, and email, then sorts

&#x20;   by the selected option.

\-   `EditForm` binds the form controls to `formModel` and enables

&#x20;   data-annotation validation.

\-   `SaveGymAsync()` sends POST for adding and PUT for editing.

\-   `DeleteGymAsync()` asks for confirmation before sending a DELETE

&#x20;   request.

\-   `ShowMessageAsync()` calls JavaScript to display operation messages.



The endpoint paths and JSON response shape must match the backend API.

The API responses are expected to contain `Success` and `Message`.



\*\*Compilation note:\*\* In a standard Blazor project, C# class

declarations cannot be placed directly inside the component's `@code`

block. Move `GymCardModel`, `GymFormModel`, and `OperationResponse` into

separate `.cs` files and import their namespace.



\### File 2: GymCard.razor




razor

@GymName
@Status

@Address, @CityName

@phone

@Email

@OpeningTime.ToString(@"hh\\:mm") - @ClosingTime.ToString(@"hh\\:mm")

Class capacity: @TotalClassCapacity

Locker: @(LockerFacility ? "Yes" : "No") Parking: @(ParkingAvailable ? "Yes" : "No") Edit Delete

@code {

[Parameter]

public Guid Id { get; set; }

[Parameter]

public string GymName { get; set; } = string.Empty;

[Parameter]

public string Address { get; set; } = string.Empty;

[Parameter]

public string CityName { get; set; } = string.Empty;

[Parameter]

public string Phone { get; set; } = string.Empty;

[Parameter]

public string Email { get; set; } = string.Empty;

[Parameter]

public TimeSpan OpeningTime { get; set; }

[Parameter]

public TimeSpan ClosingTime { get; set; }

[Parameter]

public int TotalClassCapacity { get; set; }

[Parameter]

public bool LockerFacility { get; set; }

[Parameter]

public bool ParkingAvailable { get; set; }

[Parameter]

public string Status { get; set; } = "Active";

[Parameter]

public EventCallback OnEdit { get; set; }

[Parameter]

public EventCallback OnDelete { get; set; }

private Task EditGym()

{

return OnEdit.InvokeAsync(Id);

}

private Task DeleteGym()

{

return OnDelete.InvokeAsync(Id);

}

}


### Explanation of GymCard.razor

The `\[Parameter]` properties receive the gym's data from the parent. The
`EventCallback<Guid>` properties allow the child to send the selected
gym's ID to the parent when Edit or Delete is clicked.

The card handles presentation and events, while the parent handles API
calls and business operations.

### File 3: app.css


css

/* Manage Gyms page */

.gym-card {

border: 1px solid #e9ecef;

border-radius: 14px;

overflow: hidden;

background: #fff;

transition: transform 0.2s ease, box-shadow 0.2s ease;

}

.gym-card:hover {

transform: translateY(-3px);

box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);

}

.gym-card .card-body {

padding: 1.25rem;

}

.gym-card .card-title {

font-size: 1.1rem;

overflow-wrap: anywhere;

}

.gym-detail {

color: #5f6b7a;

font-size: 0.92rem;

margin-bottom: 0.75rem;

overflow-wrap: anywhere;

}

.gym-detail i {

color: #0d6efd;

margin-right: 0.45rem;

}

.facility-badge {

display: inline-block;

padding: 0.4rem 0.6rem;

border-radius: 6px;

background: #f1f5f9;

color: #334155;

font-size: 0.8rem;

}

.gym-card .btn {

border-radius: 7px;

}

/* Add/Edit modal */

.modal-backdrop-custom {

position: fixed;

inset: 0;

z-index: 1050;

display: flex;

justify-content: center;

align-items: center;

padding: 1rem;

background: rgba(15, 23, 42, 0.6);

overflow-y: auto;

}

.form-modal {

width: 100%;

max-width: 850px;

max-height: 90vh;

overflow-y: auto;

border-radius: 12px;

}

.form-modal .card-header {

padding: 1rem 1.25rem;

}

.form-modal .card-body {

padding: 1.5rem;

}

/* Empty grid state */

.empty-state {

background: #fff;

border: 1px dashed #ced4da;

border-radius: 12px;

}

/* Responsive adjustments */

@media (max-width: 576px) {

.form-modal {

max-height: 95vh;

}

.form-modal .card-body {

padding: 1rem;

}

.gym-card .card-body {

padding: 1rem;

}

}



Bootstrap CSS and Bootstrap Icons must also be available in the
application. The custom stylesheet complements Bootstrap rather than
replacing it.

## 4. How the Three Files Work Together

1.  `ManageGyms.razor` loads gym records from the API.
2.  It passes each gym's properties to `GymCard.razor`.
3.  The child displays the details and raises Edit/Delete events.
4.  The parent opens the edit form or confirms deletion.
5.  The parent sends the API request and reloads the list after success.
6.  `app.css` styles the cards and modal.

The backend must enforce the business rules, including preventing
subscriptions for inactive or soft-deleted gyms and preventing deletion
when active subscriptions exist.

## 5. Ending Demo: Parent-Child Communication with EventCallback

A child component can notify its parent that an action occurred by using
`EventCallback`.

### Child component: NotifyButton.razor


razor

Show Notification

@code {

[Parameter]

public EventCallback OnNotify { get; set; }

private Task NotifyParent()

{

return OnNotify.InvokeAsync();

}

}


### Parent component


razor

Notifications

@message

@code {

private string message = "No notification yet.";

private void HandleNotification()

{

message = "The child component sent a notification!";

}

}

Top comments (0)