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
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
  Admin role, assuming authorization is configured.
\- `OnInitializedAsync()` loads the gyms when the component
  initializes.
\- `FilteredGyms` filters by name, city, phone, and email, then sorts
  by the selected option.
\- `EditForm` binds the form controls to `formModel` and enables
  data-annotation validation.
\- `SaveGymAsync()` sends POST for adding and PUT for editing.
\- `DeleteGymAsync()` asks for confirmation before sending a DELETE
  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
@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
@code {
private string message = "No notification yet.";
private void HandleNotification()
{
message = "The child component sent a notification!";
}
}
Top comments (0)