From 806138def64ba194bcfa765831fb177dc95474fd Mon Sep 17 00:00:00 2001 From: robgit21 Date: Wed, 6 May 2026 14:47:16 +0200 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- apps/api/Api.csproj | 19 + .../20260506065055_InitialCreate.Designer.cs | 48 + .../20260506065055_InitialCreate.cs | 37 + .../Migrations/AppDbContextModelSnapshot.cs | 45 + apps/api/Program.cs | 75 + apps/api/Properties/launchSettings.json | 38 + apps/api/Workout.cs | 8 + apps/api/appsettings.Development.json | 8 + apps/api/appsettings.json | 9 + apps/api/fitness.db-shm | Bin 0 -> 32768 bytes apps/api/fitness.db-wal | Bin 0 -> 32992 bytes apps/web/index.html | 8 +- apps/web/nswag.json | 30 + apps/web/package.json | 7 +- apps/web/src/App.tsx | 212 +- apps/web/src/api/client.ts | 54 + apps/web/vite.config.ts | 35 +- fitness-app.sln | 29 + pnpm-lock.yaml | 3223 ++++++++++++++++- 19 files changed, 3756 insertions(+), 129 deletions(-) create mode 100644 apps/api/Api.csproj create mode 100644 apps/api/Migrations/20260506065055_InitialCreate.Designer.cs create mode 100644 apps/api/Migrations/20260506065055_InitialCreate.cs create mode 100644 apps/api/Migrations/AppDbContextModelSnapshot.cs create mode 100644 apps/api/Program.cs create mode 100644 apps/api/Properties/launchSettings.json create mode 100644 apps/api/Workout.cs create mode 100644 apps/api/appsettings.Development.json create mode 100644 apps/api/appsettings.json create mode 100644 apps/api/fitness.db-shm create mode 100644 apps/api/fitness.db-wal create mode 100644 apps/web/nswag.json create mode 100644 apps/web/src/api/client.ts create mode 100644 fitness-app.sln diff --git a/apps/api/Api.csproj b/apps/api/Api.csproj new file mode 100644 index 0000000..1483228 --- /dev/null +++ b/apps/api/Api.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/apps/api/Migrations/20260506065055_InitialCreate.Designer.cs b/apps/api/Migrations/20260506065055_InitialCreate.Designer.cs new file mode 100644 index 0000000..d3bfbcf --- /dev/null +++ b/apps/api/Migrations/20260506065055_InitialCreate.Designer.cs @@ -0,0 +1,48 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260506065055_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.14"); + + modelBuilder.Entity("Workout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Workouts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/api/Migrations/20260506065055_InitialCreate.cs b/apps/api/Migrations/20260506065055_InitialCreate.cs new file mode 100644 index 0000000..fd4a560 --- /dev/null +++ b/apps/api/Migrations/20260506065055_InitialCreate.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Workouts", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Date = table.Column(type: "TEXT", nullable: false), + DurationMinutes = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Workouts", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Workouts"); + } + } +} diff --git a/apps/api/Migrations/AppDbContextModelSnapshot.cs b/apps/api/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..a401bf3 --- /dev/null +++ b/apps/api/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,45 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.14"); + + modelBuilder.Entity("Workout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Workouts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/api/Program.cs b/apps/api/Program.cs new file mode 100644 index 0000000..f7a40fd --- /dev/null +++ b/apps/api/Program.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => + options.UseSqlite("Data Source=fitness.db")); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApiDocument(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseOpenApi(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +// =================================================================== +// CRUD ENDPUNKTE (minimal, ohne Auth) +// =================================================================== + +// CREATE +app.MapPost("/api/workouts", async (Workout workout, AppDbContext db) => +{ + workout.Id = Guid.NewGuid(); + db.Workouts.Add(workout); + await db.SaveChangesAsync(); + return Results.Created($"/api/workouts/{workout.Id}", workout); +}); + +// READ ALL +app.MapGet("/api/workouts", async (AppDbContext db) => + await db.Workouts.OrderByDescending(w => w.Date).ToListAsync()); + +// READ BY ID +app.MapGet("/api/workouts/{id:guid}", async (Guid id, AppDbContext db) => + await db.Workouts.FindAsync(id) is Workout w ? Results.Ok(w) : Results.NotFound()); + +// UPDATE +app.MapPut("/api/workouts/{id:guid}", async (Guid id, Workout input, AppDbContext db) => +{ + var workout = await db.Workouts.FindAsync(id); + if (workout is null) return Results.NotFound(); + + workout.Name = input.Name; + workout.Date = input.Date; + workout.DurationMinutes = input.DurationMinutes; + workout.Notes = input.Notes; + await db.SaveChangesAsync(); + return Results.Ok(workout); +}); + +// DELETE +app.MapDelete("/api/workouts/{id:guid}", async (Guid id, AppDbContext db) => +{ + var workout = await db.Workouts.FindAsync(id); + if (workout is null) return Results.NotFound(); + + db.Workouts.Remove(workout); + await db.SaveChangesAsync(); + return Results.NoContent(); +}); + +app.MapGet("/", () => "🏋️ Fitness API läuft!"); +app.Run(); + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + public DbSet Workouts => Set(); +} \ No newline at end of file diff --git a/apps/api/Properties/launchSettings.json b/apps/api/Properties/launchSettings.json new file mode 100644 index 0000000..1fca497 --- /dev/null +++ b/apps/api/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16915", + "sslPort": 44327 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5107", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7079;http://localhost:5107", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/apps/api/Workout.cs b/apps/api/Workout.cs new file mode 100644 index 0000000..1799356 --- /dev/null +++ b/apps/api/Workout.cs @@ -0,0 +1,8 @@ +public class Workout +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime Date { get; set; } + public int DurationMinutes { get; set; } + public string? Notes { get; set; } +} \ No newline at end of file diff --git a/apps/api/appsettings.Development.json b/apps/api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/apps/api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/apps/api/appsettings.json b/apps/api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/apps/api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/apps/api/fitness.db-shm b/apps/api/fitness.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..119628bee6bf22edf2541b29c85a66ddf7aee40f GIT binary patch literal 32768 zcmeI)y9ok85CG6U|Dl=Jgbi4Nk)dE_8Kzf+f!1JVd@YC-h!vcPV5GYuhlzohc<=^> zS(asIcm+&;9)}d2m{|zJL9E@F#bL3kHp^{!oUQkh%V|C;cIEVX%fEadzj{Xc_x={I z>$;@=sqd#+KXf2KfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly&_IDKwBx?5egh{r zGJ#(OT5-=@FQlDxf&c*m1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk U1PBlyK!5-N0t5&UAn@-34{cm4*#H0l literal 0 HcmV?d00001 diff --git a/apps/api/fitness.db-wal b/apps/api/fitness.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..4f2e7965f2eba182c5557329030c8cfbb02c2455 GIT binary patch literal 32992 zcmeI*J!lj`7{>A4jdyB5Hkd>r0TCNhWMp>sD|;5QUk8dvnyaiLIYf|%-a^1e&_=Nl z6cyAeSPSA2QG$Yv2x15b+No7aMX?lTBSH==4l$5B|1Yy!Ottgdt!5wI2`^MH8sgKK zVGJ2%JzJT+)vkT{a5{XuQW;vE)Kgp5&p)cX*mw2i{cAh)q-j4i^q>$x009ILKmY** z5I_I{1Q0-A(+C_jtBuiv=584%C(q+h)V(+pUYI8$$(0iIx+`;EW@+MQMYB~XsRAK= zA%nd#sQFIKRR@&gTdm^UskxJSS(WnoygDf)deXGt8G29%Abc|T$7>i?r_v|BOUZ9K^kQeB&7tiA&fB*srAb@U#Gwt&8lqw@m# zMj?Oz0tg_000IagfB*srAb`NX5Xh`*W7su^^~nKV?n-@*K;}BaOF|_QFU&-m$*Ih7 z>sefn&(1gh@Rxs2bH2HOd4coCmoMEtap!ZH7qILH`aA-CqYyv<0R#|0009ILKmY** V5I|rv2uut#%x?A+tZwrHKLG{u;;jGx literal 0 HcmV?d00001 diff --git a/apps/web/index.html b/apps/web/index.html index 5e3836a..be8a5f1 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,13 +1,15 @@ - + - web + + + Fitness Tracker
- + \ No newline at end of file diff --git a/apps/web/nswag.json b/apps/web/nswag.json new file mode 100644 index 0000000..703d07f --- /dev/null +++ b/apps/web/nswag.json @@ -0,0 +1,30 @@ +{ + "runtime": "Net80", + "documentGenerator": { + "fromDocument": { + "url": "http://localhost:5107/swagger/v1/swagger.json", + "output": null + } + }, + "codeGenerators": { + "openApiToTypeScriptClient": { + "className": "FitnessClient", + "typeScriptVersion": 5.0, + "template": "Fetch", + "promiseType": "Promise", + "httpClass": "HttpClient", + "dateTimeType": "Date", + "nullValue": "Undefined", + "generateClientClasses": true, + "generateClientInterfaces": false, + "exportTypes": true, + "generateDtoTypes": true, + "operationGenerationMode": "MultipleClientsFromOperationId", + "markOptionalProperties": true, + "typeStyle": "Interface", + "enumStyle": "Enum", + "useAbortSignal": false, + "output": "src/api/client.ts" + } + } +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 1436c28..1add264 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "generate-api": "dotnet nswag run nswag.json" }, "dependencies": { "@tailwindcss/vite": "^4.2.4", @@ -21,12 +22,14 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react-swc": "^4.3.0", "eslint": "^10.2.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", - "vite": "^8.0.10" + "vite": "^8.0.10", + "vite-plugin-pwa": "^1.3.0" } } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a66b5ef..f82f5a2 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,122 +1,110 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import { useEffect, useState } from "react"; +import { fitnessApi, type Workout } from "./api/client"; function App() { - const [count, setCount] = useState(0) + const [workouts, setWorkouts] = useState([]); + const [form, setForm] = useState({ + name: "", + date: new Date().toISOString().slice(0, 10), + durationMinutes: 30, + notes: "", + }); + + // Workouts laden + const loadWorkouts = async () => { + const data = await fitnessApi.getWorkouts(); + setWorkouts(data); + }; + + useEffect(() => { + loadWorkouts(); + }, []); + + // Formular absenden + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await fitnessApi.createWorkout({ + ...form, + date: new Date(form.date).toISOString(), + }); + setForm({ name: "", date: new Date().toISOString().slice(0, 10), durationMinutes: 30, notes: "" }); + loadWorkouts(); + }; + + // Workout löschen + const handleDelete = async (id: string) => { + await fitnessApi.deleteWorkout(id); + loadWorkouts(); + }; return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
+
+

🏋️ Fitness Tracker

+ + {/* FORMULAR */} +
+ setForm({ ...form, name: e.target.value })} + className="w-full p-2 rounded bg-gray-800 border border-gray-700" + required + /> + setForm({ ...form, date: e.target.value })} + className="w-full p-2 rounded bg-gray-800 border border-gray-700" + /> + setForm({ ...form, durationMinutes: +e.target.value })} + className="w-full p-2 rounded bg-gray-800 border border-gray-700" + min="1" + /> +