Compare commits

..

3 Commits

4 changed files with 100 additions and 18 deletions

2
.vscode/launch.json vendored
View File

@@ -30,7 +30,7 @@
"serverReadyAction": { "serverReadyAction": {
"action": "openExternally", "action": "openExternally",
"pattern": "\\bNow listening on:\\s+https?://\\[::\\]:(\\d+)", "pattern": "\\bNow listening on:\\s+https?://\\[::\\]:(\\d+)",
"uriFormat": "http://localhost:%s" "uriFormat": "http://localhost:%s/eHoehDhc/exercises"
}, },
"env": { "env": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@@ -36,10 +36,11 @@
<div class="list"> <div class="list">
@foreach (var exercise in ExerciseList) @foreach (var exercise in ExerciseList)
{ {
<label class="checkbox-row"> var isSelected = SelectedExerciseIds.Contains(exercise.Id);
<input type="checkbox" checked="@SelectedExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleExercise(exercise.Id)" /> <div class="list-item selectable @(isSelected ? "selected" : string.Empty)" @onclick="() => ToggleExercise(exercise.Id)">
<span>@exercise.Name</span> <div class="item-title">@exercise.Name</div>
</label> <span class="check-icon @(isSelected ? "visible" : string.Empty)" aria-hidden="true">✓</span>
</div>
} }
</div> </div>
<button class="primary" @onclick="CreateRoutineAsync" disabled="@string.IsNullOrWhiteSpace(NewRoutineName)">Save Routine</button> <button class="primary" @onclick="CreateRoutineAsync" disabled="@string.IsNullOrWhiteSpace(NewRoutineName)">Save Routine</button>
@@ -91,10 +92,11 @@
<div class="list"> <div class="list">
@foreach (var exercise in ExerciseList) @foreach (var exercise in ExerciseList)
{ {
<label class="checkbox-row"> var isSelected = EditingExerciseIds.Contains(exercise.Id);
<input type="checkbox" checked="@EditingExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleEditExercise(exercise.Id)" /> <div class="list-item selectable @(isSelected ? "selected" : string.Empty)" @onclick="() => ToggleEditExercise(exercise.Id)">
<span>@exercise.Name</span> <div class="item-title">@exercise.Name</div>
</label> <span class="check-icon @(isSelected ? "visible" : string.Empty)" aria-hidden="true">✓</span>
</div>
} }
</div> </div>
<div class="actions"> <div class="actions">
@@ -111,15 +113,20 @@
<div class="list"> <div class="list">
@foreach (var entry in RunEntries) @foreach (var entry in RunEntries)
{ {
<div class="list-item @(entry.Completed ? "done" : string.Empty)"> <div class="list-item selectable @(entry.Completed ? "selected" : string.Empty)" @onclick="() => ToggleRunCompleted(entry.ExerciseId)">
<label class="checkbox-row"> <div class="item-title">@GetExerciseName(entry.ExerciseId)</div>
<input type="checkbox" checked="@entry.Completed" @onchange="() => ToggleRunCompleted(entry.ExerciseId)" /> <div class="item-actions" @onclick:stopPropagation="true">
<span>@GetExerciseName(entry.ExerciseId)</span> <div class="input-unit">
</label> <select class="input input-sm" @bind="entry.Weight">
<div class="input-unit"> @foreach (var w in WeightOptions)
<input class="input input-sm" type="number" step="0.5" @bind="entry.Weight" @bind:event="oninput" /> {
<span class="unit">kg</span> <option value="@w">@w</option>
}
</select>
<span class="unit">kg</span>
</div>
</div> </div>
<span class="check-icon @(entry.Completed ? "visible" : string.Empty)" aria-hidden="true">✓</span>
</div> </div>
} }
</div> </div>

View File

@@ -32,6 +32,42 @@ public partial class Routines
private RoutineDto? ActiveRun { get; set; } private RoutineDto? ActiveRun { get; set; }
private List<RoutineRunEntryDto> RunEntries { get; set; } = new(); private List<RoutineRunEntryDto> RunEntries { get; set; } = new();
/// <summary>
/// Available weight options for the routine run select input.
/// 1040 kg in 2.5 kg steps, then 40100 kg in 5 kg steps.
/// </summary>
private static readonly List<double> WeightOptions = BuildWeightOptions();
/// <summary>
/// Builds the list of available weight options for the routine run select input.
/// </summary>
/// <returns>A list of double values representing weight options.</returns>
private static List<double> BuildWeightOptions()
{
var options = new List<double>();
for (var w = 10.0; w <= 40.0; w += 2.5)
{
options.Add(w);
}
for (var w = 45.0; w <= 100.0; w += 5.0)
{
options.Add(w);
}
return options;
}
/// <summary>
/// Snaps a weight value to the nearest available option.
/// </summary>
/// <param name="weight">The weight value to snap to the nearest option.</param>
/// <returns>The nearest available weight option.</returns>
private static double SnapToNearest(double weight)
{
return WeightOptions.OrderBy(w => Math.Abs(w - weight)).First();
}
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -197,7 +233,7 @@ public partial class Routines
.Select(e => .Select(e =>
{ {
var last = lastRun.Entries.FirstOrDefault(x => x.ExerciseId == e.ExerciseId); var last = lastRun.Entries.FirstOrDefault(x => x.ExerciseId == e.ExerciseId);
return new RoutineRunEntryDto(e.ExerciseId, last?.Weight ?? 0, false); return new RoutineRunEntryDto(e.ExerciseId, SnapToNearest(last?.Weight ?? WeightOptions[0]), false);
}).ToList(); }).ToList();
} }

View File

@@ -91,6 +91,8 @@ p {
.input-sm { .input-sm {
max-width: 110px; max-width: 110px;
appearance: none;
-webkit-appearance: none;
} }
.primary { .primary {
@@ -139,10 +141,47 @@ p {
border-color: #1f6b38; border-color: #1f6b38;
} }
.list-item.selectable {
cursor: pointer;
}
.list-item.selected {
background: #10331a;
border-color: #1f6b38;
}
.check-icon {
width: 26px;
height: 26px;
border-radius: 999px;
border: 1px solid #2a2a2a;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #ffffff;
opacity: 0;
transform: scale(0.85);
transition: opacity 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}
.check-icon.visible {
opacity: 1;
transform: scale(1);
border-color: #1f6b38;
}
.item-title { .item-title {
font-weight: 600; font-weight: 600;
} }
.item-actions {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.item-subtitle { .item-subtitle {
color: #bdbdbd; color: #bdbdbd;
font-size: 0.85rem; font-size: 0.85rem;