Compare commits
6 Commits
990e67e88c
...
65a13539e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
65a13539e0
|
|||
|
fd8395cc48
|
|||
|
8f2284e1fc
|
|||
|
5db6fee866
|
|||
|
01581b7a91
|
|||
|
56aacb0134
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"git commit": true
|
||||
},
|
||||
"dotnet.defaultSolution": "ASTRAIN.slnx"
|
||||
}
|
||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -24,11 +24,11 @@
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Build Docker Image",
|
||||
"label": "Build & Push Docker",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}\\.venv\\Scripts\\python.exe",
|
||||
"args": [
|
||||
"${workspaceFolder}/docker/build_image.py"
|
||||
"${workspaceFolder}/docker/build_and_push_image.py"
|
||||
],
|
||||
"problemMatcher": []
|
||||
}
|
||||
|
||||
15
docker-compose.traefik.yml
Normal file
15
docker-compose.traefik.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
astrain:
|
||||
image: git.beging.de/troogs/astrain:latest
|
||||
restart: unless-stopped
|
||||
container_name: astrain
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.fs-onboarding-si.rule=Host(`astrain.melvin.beging.de`)
|
||||
- traefik.http.services.fs-onboarding-si.loadbalancer.server.port=8080
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
26
docker/build_and_push_image.py
Normal file
26
docker/build_and_push_image.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
scripts = ["build_image.py", "push_image.py"]
|
||||
|
||||
for script in scripts:
|
||||
script_path = script_dir / script
|
||||
print(f"Executing {script}...")
|
||||
try:
|
||||
subprocess.run([sys.executable, str(script_path)], check=True)
|
||||
print(f"{script} executed successfully.\n")
|
||||
except subprocess.CalledProcessError as error:
|
||||
print(f"Error: {script} failed with exit code {error.returncode}.")
|
||||
return error.returncode
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
57
docker/push_image.py
Normal file
57
docker/push_image.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_timestamp_tag(today: dt.date) -> str:
|
||||
year_suffix = today.year % 100
|
||||
day_of_year = today.timetuple().tm_yday
|
||||
return f"{year_suffix}.{day_of_year}"
|
||||
|
||||
|
||||
def run_command(command: list[str], repo_root: Path) -> int:
|
||||
print(" ".join(command))
|
||||
result = subprocess.run(command, cwd=str(repo_root))
|
||||
return result.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
|
||||
today = dt.date.today()
|
||||
timestamp_tag = get_timestamp_tag(today)
|
||||
|
||||
local_image = "troogs/astrain"
|
||||
registry_image = "git.beging.de/troogs/astrain"
|
||||
tags = ["latest", timestamp_tag]
|
||||
|
||||
for tag in tags:
|
||||
tag_cmd = [
|
||||
"docker",
|
||||
"tag",
|
||||
f"{local_image}:{tag}",
|
||||
f"{registry_image}:{tag}",
|
||||
]
|
||||
print(f"Tagging {local_image}:{tag} as {registry_image}:{tag}")
|
||||
exit_code = run_command(tag_cmd, repo_root)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
|
||||
for tag in tags:
|
||||
push_cmd = [
|
||||
"docker",
|
||||
"push",
|
||||
f"{registry_image}:{tag}",
|
||||
]
|
||||
print(f"Pushing {registry_image}:{tag}")
|
||||
exit_code = run_command(push_cmd, repo_root)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ASTRAIN.Api.Endpoints;
|
||||
|
||||
@@ -22,9 +23,10 @@ internal static class ExerciseEndpoints
|
||||
/// <returns>The same route group for chaining.</returns>
|
||||
public static RouteGroupBuilder MapExerciseEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/users/{userId}/exercises", async (string userId, AppDbContext db) =>
|
||||
group.MapGet("/users/{userId}/exercises", async (string userId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var items = await db.Exercises
|
||||
.Where(e => e.UserId == user.Id)
|
||||
.OrderBy(e => e.Name)
|
||||
@@ -35,9 +37,10 @@ internal static class ExerciseEndpoints
|
||||
.WithSummary("List exercises")
|
||||
.WithDescription("Returns the exercises for the specified user.");
|
||||
|
||||
group.MapPost("/users/{userId}/exercises", async (string userId, ExerciseUpsertRequest request, AppDbContext db) =>
|
||||
group.MapPost("/users/{userId}/exercises", async (string userId, ExerciseUpsertRequest request, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return Results.BadRequest("Name is required.");
|
||||
@@ -57,9 +60,10 @@ internal static class ExerciseEndpoints
|
||||
.WithSummary("Create exercise")
|
||||
.WithDescription("Creates a new exercise for the specified user.");
|
||||
|
||||
group.MapPut("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, ExerciseUpsertRequest request, AppDbContext db) =>
|
||||
group.MapPut("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, ExerciseUpsertRequest request, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var exercise = await db.Exercises.FirstOrDefaultAsync(e => e.Id == exerciseId && e.UserId == user.Id);
|
||||
if (exercise is null)
|
||||
{
|
||||
@@ -79,9 +83,10 @@ internal static class ExerciseEndpoints
|
||||
.WithSummary("Update exercise")
|
||||
.WithDescription("Updates the name of an exercise for the specified user.");
|
||||
|
||||
group.MapDelete("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, AppDbContext db) =>
|
||||
group.MapDelete("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var exercise = await db.Exercises.FirstOrDefaultAsync(e => e.Id == exerciseId && e.UserId == user.Id);
|
||||
if (exercise is null)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ASTRAIN.Api.Endpoints;
|
||||
|
||||
@@ -22,9 +23,10 @@ internal static class RoutineEndpoints
|
||||
/// <returns>The same route group for chaining.</returns>
|
||||
public static RouteGroupBuilder MapRoutineEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/users/{userId}/routines", async (string userId, AppDbContext db) =>
|
||||
group.MapGet("/users/{userId}/routines", async (string userId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var routines = await db.Routines
|
||||
.Include(r => r.Exercises)
|
||||
.ThenInclude(re => re.Exercise)
|
||||
@@ -46,9 +48,10 @@ internal static class RoutineEndpoints
|
||||
.WithSummary("List routines")
|
||||
.WithDescription("Returns all routines for the specified user.");
|
||||
|
||||
group.MapGet("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db) =>
|
||||
group.MapGet("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var routine = await db.Routines
|
||||
.Include(r => r.Exercises)
|
||||
.ThenInclude(re => re.Exercise)
|
||||
@@ -72,9 +75,10 @@ internal static class RoutineEndpoints
|
||||
.WithSummary("Get routine")
|
||||
.WithDescription("Returns a specific routine and its exercises.");
|
||||
|
||||
group.MapPost("/users/{userId}/routines", async (string userId, RoutineUpsertRequest request, AppDbContext db) =>
|
||||
group.MapPost("/users/{userId}/routines", async (string userId, RoutineUpsertRequest request, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return Results.BadRequest("Name is required.");
|
||||
@@ -113,9 +117,10 @@ internal static class RoutineEndpoints
|
||||
.WithSummary("Create routine")
|
||||
.WithDescription("Creates a routine and associates exercises with it.");
|
||||
|
||||
group.MapPut("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, RoutineUpsertRequest request, AppDbContext db) =>
|
||||
group.MapPut("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, RoutineUpsertRequest request, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var routine = await db.Routines
|
||||
.Include(r => r.Exercises)
|
||||
.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
|
||||
@@ -160,9 +165,10 @@ internal static class RoutineEndpoints
|
||||
.WithSummary("Update routine")
|
||||
.WithDescription("Updates routine metadata and exercise ordering.");
|
||||
|
||||
group.MapDelete("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db) =>
|
||||
group.MapDelete("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var routine = await db.Routines.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
|
||||
if (routine is null)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ASTRAIN.Api.Endpoints;
|
||||
|
||||
@@ -22,9 +23,10 @@ internal static class RoutineRunEndpoints
|
||||
/// <returns>The same route group for chaining.</returns>
|
||||
public static RouteGroupBuilder MapRoutineRunEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/users/{userId}/routines/{routineId:int}/last-run", async (string userId, int routineId, AppDbContext db) =>
|
||||
group.MapGet("/users/{userId}/routines/{routineId:int}/last-run", async (string userId, int routineId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var lastRun = await db.RoutineRuns
|
||||
.Include(rr => rr.Entries)
|
||||
.Where(rr => rr.UserId == user.Id && rr.RoutineId == routineId)
|
||||
@@ -47,9 +49,10 @@ internal static class RoutineRunEndpoints
|
||||
.WithSummary("Get last routine run")
|
||||
.WithDescription("Returns the most recent run summary for a routine.");
|
||||
|
||||
group.MapPost("/users/{userId}/routines/{routineId:int}/runs", async (string userId, int routineId, RoutineRunRequest request, AppDbContext db) =>
|
||||
group.MapPost("/users/{userId}/routines/{routineId:int}/runs", async (string userId, int routineId, RoutineRunRequest request, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
var routine = await db.Routines.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
|
||||
if (routine is null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using ASTRAIN.Shared.Responses;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ASTRAIN.Api.Endpoints;
|
||||
|
||||
@@ -19,17 +20,19 @@ internal static class UserEndpoints
|
||||
/// <returns>The same route group for chaining.</returns>
|
||||
public static RouteGroupBuilder MapUserEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/users/ensure", async (string? userId, AppDbContext db) =>
|
||||
group.MapGet("/users/ensure", async (string? userId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
return Results.Ok(new EnsureUserResponse(user.Id));
|
||||
})
|
||||
.WithSummary("Ensure user")
|
||||
.WithDescription("Ensures a user exists and returns the user id.");
|
||||
|
||||
group.MapGet("/users/{userId}", async (string userId, AppDbContext db) =>
|
||||
group.MapGet("/users/{userId}", async (string userId, AppDbContext db, IConfiguration config) =>
|
||||
{
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||
var populateSampleData = config.GetValue("SampleData:Enabled", false);
|
||||
var user = await UserProvisioning.EnsureUserAsync(db, userId, populateSampleData);
|
||||
return Results.Ok(new EnsureUserResponse(user.Id));
|
||||
})
|
||||
.WithSummary("Get user")
|
||||
|
||||
@@ -15,10 +15,6 @@ builder.Services.AddCors(options =>
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
|
||||
// policy.WithOrigins("http://localhost:5016", "http://:5016", "https://localhost:7252")
|
||||
// .AllowAnyHeader()
|
||||
// .AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7049;http://localhost:5055",
|
||||
"applicationUrl": "https://+:7049;http://+:5055",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ internal static class UserProvisioning
|
||||
/// </summary>
|
||||
/// <param name="db">The application database context.</param>
|
||||
/// <param name="userId">Optional user id to validate or create.</param>
|
||||
/// <param name="populateSampleData">Whether to populate sample data for new users.</param>
|
||||
/// <returns>The existing or newly created user.</returns>
|
||||
public static async Task<User> EnsureUserAsync(AppDbContext db, string? userId)
|
||||
public static async Task<User> EnsureUserAsync(AppDbContext db, string? userId, bool populateSampleData)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userId) && IsValidUserId(userId))
|
||||
{
|
||||
@@ -29,7 +30,10 @@ internal static class UserProvisioning
|
||||
var created = new User { Id = userId };
|
||||
db.Users.Add(created);
|
||||
await db.SaveChangesAsync();
|
||||
if (populateSampleData)
|
||||
{
|
||||
await PopulateSampleDataAsync(db, created);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -45,7 +49,10 @@ internal static class UserProvisioning
|
||||
var user = new User { Id = newId };
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
if (populateSampleData)
|
||||
{
|
||||
await PopulateSampleDataAsync(db, user);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"SampleData": {
|
||||
"Enabled": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,8 @@
|
||||
"ConnectionStrings": {
|
||||
"Default": "Data Source=Data/astrain.db"
|
||||
},
|
||||
"SampleData": {
|
||||
"Enabled": false
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -65,102 +65,3 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
private List<ExerciseDto> ExerciseList { get; set; } = new();
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool ShowCreateExercise { get; set; }
|
||||
private string NewExerciseName { get; set; } = string.Empty;
|
||||
private int? EditingId { get; set; }
|
||||
private string EditingName { get; set; } = string.Empty;
|
||||
|
||||
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}/exercises", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadExercisesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadExercisesAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ExerciseList = await Api.GetExercisesAsync(UserContext.UserId);
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
private async Task CreateExerciseAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewExerciseName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Api.CreateExerciseAsync(UserContext.UserId, new ExerciseUpsertRequest(NewExerciseName));
|
||||
if (result is not null)
|
||||
{
|
||||
ExerciseList.Add(result);
|
||||
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
|
||||
NewExerciseName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleCreate()
|
||||
{
|
||||
ShowCreateExercise = !ShowCreateExercise;
|
||||
}
|
||||
|
||||
private void StartEdit(ExerciseDto exercise)
|
||||
{
|
||||
EditingId = exercise.Id;
|
||||
EditingName = exercise.Name;
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
EditingId = null;
|
||||
EditingName = string.Empty;
|
||||
}
|
||||
|
||||
private async Task SaveEditAsync(int exerciseId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EditingName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Api.UpdateExerciseAsync(UserContext.UserId, exerciseId, new ExerciseUpsertRequest(EditingName));
|
||||
if (result is not null)
|
||||
{
|
||||
var index = ExerciseList.FindIndex(e => e.Id == exerciseId);
|
||||
if (index >= 0)
|
||||
{
|
||||
ExerciseList[index] = result;
|
||||
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
CancelEdit();
|
||||
}
|
||||
|
||||
private async Task DeleteExerciseAsync(int exerciseId)
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this exercise?");
|
||||
if (!confirmed) return;
|
||||
|
||||
await Api.DeleteExerciseAsync(UserContext.UserId, exerciseId);
|
||||
ExerciseList.RemoveAll(e => e.Id == exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
159
src/ASTRAIN.Client/Pages/Exercises.razor.cs
Normal file
159
src/ASTRAIN.Client/Pages/Exercises.razor.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using ASTRAIN.Shared.Dtos;
|
||||
using ASTRAIN.Shared.Requests;
|
||||
|
||||
namespace ASTRAIN.Client.Pages;
|
||||
|
||||
public partial class Exercises
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional user id from route.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of exercises for the current user.
|
||||
/// </summary>
|
||||
private List<ExerciseDto> ExerciseList { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the page is currently loading data.
|
||||
/// </summary>
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the create exercise UI is visible.
|
||||
/// </summary>
|
||||
private bool ShowCreateExercise { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name for a new exercise being created.
|
||||
/// </summary>
|
||||
private string NewExerciseName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The currently edited exercise id, if any.
|
||||
/// </summary>
|
||||
private int? EditingId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current edited exercise name.
|
||||
/// </summary>
|
||||
private string EditingName { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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}/exercises", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadExercisesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads exercises from the API.
|
||||
/// </summary>
|
||||
private async Task LoadExercisesAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ExerciseList = await Api.GetExercisesAsync(UserContext.UserId);
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new exercise with the provided name.
|
||||
/// </summary>
|
||||
private async Task CreateExerciseAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewExerciseName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Api.CreateExerciseAsync(UserContext.UserId, new ExerciseUpsertRequest(NewExerciseName));
|
||||
if (result is not null)
|
||||
{
|
||||
ExerciseList.Add(result);
|
||||
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
|
||||
NewExerciseName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the create form visibility.
|
||||
/// </summary>
|
||||
private void ToggleCreate()
|
||||
{
|
||||
ShowCreateExercise = !ShowCreateExercise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin editing the supplied exercise.
|
||||
/// </summary>
|
||||
private void StartEdit(ExerciseDto exercise)
|
||||
{
|
||||
EditingId = exercise.Id;
|
||||
EditingName = exercise.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the current edit.
|
||||
/// </summary>
|
||||
private void CancelEdit()
|
||||
{
|
||||
EditingId = null;
|
||||
EditingName = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the edited exercise name.
|
||||
/// </summary>
|
||||
private async Task SaveEditAsync(int exerciseId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EditingName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Api.UpdateExerciseAsync(UserContext.UserId, exerciseId, new ExerciseUpsertRequest(EditingName));
|
||||
if (result is not null)
|
||||
{
|
||||
var index = ExerciseList.FindIndex(e => e.Id == exerciseId);
|
||||
if (index >= 0)
|
||||
{
|
||||
ExerciseList[index] = result;
|
||||
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
CancelEdit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the exercise with the given id after confirmation.
|
||||
/// </summary>
|
||||
private async Task DeleteExerciseAsync(int exerciseId)
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this exercise?");
|
||||
if (!confirmed) return;
|
||||
|
||||
await Api.DeleteExerciseAsync(UserContext.UserId, exerciseId);
|
||||
ExerciseList.RemoveAll(e => e.Id == exerciseId);
|
||||
}
|
||||
}
|
||||
@@ -14,23 +14,3 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var ensured = await Api.EnsureUserAsync(UserId);
|
||||
if (string.IsNullOrWhiteSpace(ensured))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UserContext.SetUserId(ensured);
|
||||
var target = $"/{ensured}/routines";
|
||||
if (!Navigation.Uri.EndsWith(target, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Navigation.NavigateTo(target, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
src/ASTRAIN.Client/Pages/Home.razor.cs
Normal file
31
src/ASTRAIN.Client/Pages/Home.razor.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace ASTRAIN.Client.Pages;
|
||||
|
||||
public partial class Home
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional user id from route.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var ensured = await Api.EnsureUserAsync(UserId);
|
||||
if (string.IsNullOrWhiteSpace(ensured))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UserContext.SetUserId(ensured);
|
||||
var target = $"/{ensured}/routines";
|
||||
if (!Navigation.Uri.EndsWith(target, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Navigation.NavigateTo(target, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,188 +129,3 @@
|
||||
}
|
||||
</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 DeleteRoutineAsync(int routineId)
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this routine?");
|
||||
if (!confirmed) return;
|
||||
|
||||
await Api.DeleteRoutineAsync(UserContext.UserId, routineId);
|
||||
RoutineList.RemoveAll(r => r.Id == routineId);
|
||||
}
|
||||
|
||||
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 async Task AbortRun()
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to abort this routine run?");
|
||||
if (!confirmed) return;
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
||||
261
src/ASTRAIN.Client/Pages/Routines.razor.cs
Normal file
261
src/ASTRAIN.Client/Pages/Routines.razor.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using ASTRAIN.Shared.Dtos;
|
||||
using ASTRAIN.Shared.Requests;
|
||||
|
||||
namespace ASTRAIN.Client.Pages;
|
||||
|
||||
public partial class Routines
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional user id from route.
|
||||
/// </summary>
|
||||
[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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads exercises and routines from the API and updates the UI state.
|
||||
/// </summary>
|
||||
|
||||
private void ToggleExercise(int exerciseId)
|
||||
{
|
||||
if (!SelectedExerciseIds.Add(exerciseId))
|
||||
{
|
||||
SelectedExerciseIds.Remove(exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles whether an exercise is selected when creating a routine.
|
||||
/// </summary>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new routine with the selected exercises.
|
||||
/// </summary>
|
||||
}
|
||||
|
||||
private void ToggleCreate()
|
||||
{
|
||||
ShowCreateRoutine = !ShowCreateRoutine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle visibility of the create-routine UI.
|
||||
/// </summary>
|
||||
|
||||
private void GoToExercises()
|
||||
{
|
||||
Navigation.NavigateTo($"/{UserContext.UserId}/exercises");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the exercises page for the current user.
|
||||
/// </summary>
|
||||
|
||||
private void StartEdit(RoutineDto routine)
|
||||
{
|
||||
EditingRoutine = routine;
|
||||
EditingName = routine.Name;
|
||||
EditingExerciseIds = routine.Exercises.Select(e => e.ExerciseId).ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin editing the supplied routine.
|
||||
/// </summary>
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
EditingRoutine = null;
|
||||
EditingName = string.Empty;
|
||||
EditingExerciseIds.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the current routine edit and reset state.
|
||||
/// </summary>
|
||||
|
||||
private void ToggleEditExercise(int exerciseId)
|
||||
{
|
||||
if (!EditingExerciseIds.Add(exerciseId))
|
||||
{
|
||||
EditingExerciseIds.Remove(exerciseId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle whether an exercise is selected in the routine edit UI.
|
||||
/// </summary>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save changes made to the currently edited routine.
|
||||
/// </summary>
|
||||
|
||||
private async Task DeleteRoutineAsync(int routineId)
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this routine?");
|
||||
if (!confirmed) return;
|
||||
|
||||
await Api.DeleteRoutineAsync(UserContext.UserId, routineId);
|
||||
RoutineList.RemoveAll(r => r.Id == routineId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the routine with the given id after confirmation.
|
||||
/// </summary>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a routine run and prepare run entries.
|
||||
/// </summary>
|
||||
|
||||
private void ToggleRunCompleted(int exerciseId)
|
||||
{
|
||||
var entry = RunEntries.FirstOrDefault(e => e.ExerciseId == exerciseId);
|
||||
if (entry is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
entry.Completed = !entry.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the completion state of a run entry.
|
||||
/// </summary>
|
||||
|
||||
private string GetExerciseName(int exerciseId)
|
||||
{
|
||||
return ExerciseList.FirstOrDefault(e => e.Id == exerciseId)?.Name ?? "Exercise";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the display name for an exercise id.
|
||||
/// </summary>
|
||||
|
||||
private async Task AbortRun()
|
||||
{
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to abort this routine run?");
|
||||
if (!confirmed) return;
|
||||
|
||||
ActiveRun = null;
|
||||
RunEntries = new List<RoutineRunEntryDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abort the active routine run after confirmation.
|
||||
/// </summary>
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the current routine run to the API.
|
||||
/// </summary>
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
@page "/weather"
|
||||
@inject HttpClient Http
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates fetching data from the server.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
||||
}
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public string? Summary { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ApiBaseAddress": "http://localhost:5055"
|
||||
"ApiBaseAddress": "http://10.20.30.99:5055"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user