feat: build blazor client UI and PWA assets
This commit is contained in:
296
src/ASTRAIN.Client/Pages/Routines.razor
Normal file
296
src/ASTRAIN.Client/Pages/Routines.razor
Normal file
@@ -0,0 +1,296 @@
|
||||
@page "/routines"
|
||||
@page "/{UserId}/routines"
|
||||
|
||||
@inject ApiClient Api
|
||||
@inject NavigationManager Navigation
|
||||
@inject UserContext UserContext
|
||||
|
||||
<PageTitle>Routines</PageTitle>
|
||||
|
||||
<div class="page">
|
||||
<header class="page-header">
|
||||
<h1>Routines</h1>
|
||||
<p>Build routines from your exercise list.</p>
|
||||
<button class="primary" @onclick="ToggleCreate">@(ShowCreateRoutine ? "Close" : "+")</button>
|
||||
</header>
|
||||
|
||||
@if (ActiveRun is null)
|
||||
{
|
||||
@if (ExerciseList.Count == 0)
|
||||
{
|
||||
<section class="card">
|
||||
<h2>No exercises yet</h2>
|
||||
<p class="muted">Add exercises first, then create routines.</p>
|
||||
<button class="primary" @onclick="GoToExercises">Go to Exercises</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (ShowCreateRoutine)
|
||||
{
|
||||
<section class="card">
|
||||
<h2>Create Routine</h2>
|
||||
<input class="input" placeholder="Routine name" @bind="NewRoutineName" @bind:event="oninput" />
|
||||
<div class="list">
|
||||
@foreach (var exercise in ExerciseList)
|
||||
{
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" checked="@SelectedExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleExercise(exercise.Id)" />
|
||||
<span>@exercise.Name</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
<button class="primary" @onclick="CreateRoutineAsync" disabled="@string.IsNullOrWhiteSpace(NewRoutineName)">Save Routine</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="card">
|
||||
<h2>Your Routines</h2>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
else if (RoutineList.Count == 0)
|
||||
{
|
||||
<p class="muted">No routines yet. Create one above.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list">
|
||||
@foreach (var routine in RoutineList)
|
||||
{
|
||||
<div class="list-item">
|
||||
<div>
|
||||
<div class="item-title">@routine.Name</div>
|
||||
<div class="item-subtitle">@string.Join(" · ", routine.Exercises.Select(e => e.Name))</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ghost" @onclick="() => StartEdit(routine)">Edit</button>
|
||||
<button class="primary" @onclick="() => StartRun(routine)">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (EditingRoutine is not null)
|
||||
{
|
||||
<section class="card">
|
||||
<h2>Edit Routine</h2>
|
||||
<input class="input" @bind="EditingName" @bind:event="oninput" />
|
||||
<div class="list">
|
||||
@foreach (var exercise in ExerciseList)
|
||||
{
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" checked="@EditingExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleEditExercise(exercise.Id)" />
|
||||
<span>@exercise.Name</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" @onclick="SaveEditAsync">Save Changes</button>
|
||||
<button class="ghost" @onclick="CancelEdit">Cancel</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="card">
|
||||
<h2>Routine Run: @ActiveRun.Name</h2>
|
||||
<div class="list">
|
||||
@foreach (var entry in RunEntries)
|
||||
{
|
||||
<div class="list-item @(entry.Completed ? "done" : string.Empty)">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" checked="@entry.Completed" @onchange="() => ToggleRunCompleted(entry.ExerciseId)" />
|
||||
<span>@GetExerciseName(entry.ExerciseId)</span>
|
||||
</label>
|
||||
<div class="input-unit">
|
||||
<input class="input input-sm" type="number" step="0.5" @bind="entry.Weight" @bind:event="oninput" />
|
||||
<span class="unit">kg</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ghost" @onclick="AbortRun">Abort</button>
|
||||
<button class="primary" @onclick="SaveRunAsync">Save Run</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
private List<ExerciseDto> ExerciseList { get; set; } = new();
|
||||
private List<RoutineDto> RoutineList { get; set; } = new();
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool ShowCreateRoutine { get; set; }
|
||||
|
||||
private string NewRoutineName { get; set; } = string.Empty;
|
||||
private HashSet<int> SelectedExerciseIds { get; set; } = new();
|
||||
|
||||
private RoutineDto? EditingRoutine { get; set; }
|
||||
private string EditingName { get; set; } = string.Empty;
|
||||
private HashSet<int> EditingExerciseIds { get; set; } = new();
|
||||
|
||||
private RoutineDto? ActiveRun { get; set; }
|
||||
private List<RoutineRunEntryDto> RunEntries { get; set; } = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var ensured = await Api.EnsureUserAsync(UserId);
|
||||
if (string.IsNullOrWhiteSpace(ensured))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UserContext.SetUserId(ensured);
|
||||
if (UserId != ensured)
|
||||
{
|
||||
Navigation.NavigateTo($"/{ensured}/routines", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ExerciseList = await Api.GetExercisesAsync(UserContext.UserId);
|
||||
RoutineList = await Api.GetRoutinesAsync(UserContext.UserId);
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
private void ToggleExercise(int exerciseId)
|
||||
{
|
||||
if (!SelectedExerciseIds.Add(exerciseId))
|
||||
{
|
||||
SelectedExerciseIds.Remove(exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateRoutineAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewRoutineName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new RoutineUpsertRequest(NewRoutineName, SelectedExerciseIds.ToList());
|
||||
var created = await Api.CreateRoutineAsync(UserContext.UserId, request);
|
||||
if (created is not null)
|
||||
{
|
||||
RoutineList.Add(created);
|
||||
RoutineList = RoutineList.OrderBy(r => r.Name).ToList();
|
||||
NewRoutineName = string.Empty;
|
||||
SelectedExerciseIds.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleCreate()
|
||||
{
|
||||
ShowCreateRoutine = !ShowCreateRoutine;
|
||||
}
|
||||
|
||||
private void GoToExercises()
|
||||
{
|
||||
Navigation.NavigateTo($"/{UserContext.UserId}/exercises");
|
||||
}
|
||||
|
||||
private void StartEdit(RoutineDto routine)
|
||||
{
|
||||
EditingRoutine = routine;
|
||||
EditingName = routine.Name;
|
||||
EditingExerciseIds = routine.Exercises.Select(e => e.ExerciseId).ToHashSet();
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
EditingRoutine = null;
|
||||
EditingName = string.Empty;
|
||||
EditingExerciseIds.Clear();
|
||||
}
|
||||
|
||||
private void ToggleEditExercise(int exerciseId)
|
||||
{
|
||||
if (!EditingExerciseIds.Add(exerciseId))
|
||||
{
|
||||
EditingExerciseIds.Remove(exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveEditAsync()
|
||||
{
|
||||
if (EditingRoutine is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new RoutineUpsertRequest(EditingName, EditingExerciseIds.ToList());
|
||||
var updated = await Api.UpdateRoutineAsync(UserContext.UserId, EditingRoutine.Id, request);
|
||||
if (updated is not null)
|
||||
{
|
||||
var index = RoutineList.FindIndex(r => r.Id == EditingRoutine.Id);
|
||||
if (index >= 0)
|
||||
{
|
||||
RoutineList[index] = updated;
|
||||
RoutineList = RoutineList.OrderBy(r => r.Name).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
CancelEdit();
|
||||
}
|
||||
|
||||
private async Task StartRun(RoutineDto routine)
|
||||
{
|
||||
ActiveRun = routine;
|
||||
var lastRun = await Api.GetLastRunAsync(UserContext.UserId, routine.Id);
|
||||
RunEntries = routine.Exercises
|
||||
.OrderBy(e => e.Order)
|
||||
.Select(e =>
|
||||
{
|
||||
var last = lastRun.Entries.FirstOrDefault(x => x.ExerciseId == e.ExerciseId);
|
||||
return new RoutineRunEntryDto(e.ExerciseId, last?.Weight ?? 0, false);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private void ToggleRunCompleted(int exerciseId)
|
||||
{
|
||||
var entry = RunEntries.FirstOrDefault(e => e.ExerciseId == exerciseId);
|
||||
if (entry is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
entry.Completed = !entry.Completed;
|
||||
}
|
||||
|
||||
private string GetExerciseName(int exerciseId)
|
||||
{
|
||||
return ExerciseList.FirstOrDefault(e => e.Id == exerciseId)?.Name ?? "Exercise";
|
||||
}
|
||||
|
||||
private void AbortRun()
|
||||
{
|
||||
ActiveRun = null;
|
||||
RunEntries = new List<RoutineRunEntryDto>();
|
||||
}
|
||||
|
||||
private async Task SaveRunAsync()
|
||||
{
|
||||
if (ActiveRun is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new RoutineRunRequest(RunEntries);
|
||||
await Api.SaveRunAsync(UserContext.UserId, ActiveRun.Id, request);
|
||||
ActiveRun = null;
|
||||
RunEntries = new List<RoutineRunEntryDto>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user