20 Commits

Author SHA1 Message Date
cec2b65c29 Set WebRootPath, enable static file serving, and upgrade to .NET 9.0 2026-01-23 20:49:51 +01:00
23505060c5 Update Vite build output and remove test file
Added Vite build output directories to .gitignore and configured Vite to output to ../wwwroot. Removed obsolete App.spec.ts test file from GUI/src/__tests__.
2025-12-31 16:14:20 +01:00
Jonas
c63da03a3e Füge Datenbank-WAL- und SHM-Dateien zur .gitignore hinzu 2025-12-08 16:40:25 +00:00
Jonas
94fcf83241 Füge Datenbank- und S3-Dienste hinzu; entferne nicht mehr benötigte WAL- und SHM-Dateien 2025-12-08 16:37:49 +00:00
Jonas
6cc45313f5 Update database files to reflect recent changes 2025-12-08 16:34:40 +00:00
aacd8b7d96 Change AgeGroup ID type from int to string and add migration
Refactored AgeGroupController, AgeGroupService, and IAgeGroupService to use string IDs instead of int. Added initial Entity Framework migration and database files to support AltersGruppe with string primary key.
2025-12-07 20:32:49 +01:00
9128b199e9 Switch AltersGruppe ID to ULID and add config
Changed AltersGruppe model ID from int to string and now generates ULID automatically. Added AltersGruppeConfiguration for EF Core mapping. Updated ApplicationDbContext to apply configurations from assembly. Removed obsolete migration files and database artifacts. Added Ulid package dependency.
2025-12-07 19:48:47 +01:00
3125d657dd Simple Database + Endpoints 2025-12-07 12:07:10 +01:00
35eee78efc Enhance Home page with new sections and image presets
Added new event images and introduced HomeEntrieWithImagePreset component for consistent image-text layouts. Updated Home.vue to include new sections for club values and events (Nikolausturnier, Wettkämpfe, Kinoabende) with tab navigation. Refactored carousel and improved HomeEntrie for better mobile/desktop title handling. Renamed CarouseWithTitle.vue to CarouselItemWithTitle.vue for clarity.
2025-11-23 20:53:33 +01:00
c2d84ad5e0 Fix Auto Theme 2025-11-22 22:57:05 +01:00
99af5fe13e Home Page
+ Added Information
+ First and Second Entrie
2025-11-22 22:50:05 +01:00
a118026b04 Main Site Work
+ Updatet Router
+ "Hauptseite" mit Großem Bild
2025-11-22 17:04:45 +01:00
30eeaabc5a Update app title and enhance app bar UI
Changed the HTML title to 'Judoteam - Stadtlohn'. Improved the app bar in Layout.vue by replacing search and heart icons with account and theme toggle buttons, each wrapped in tooltips for better user guidance. Added rounded styling to navigation drawer items.
2025-11-09 21:01:59 +01:00
4ce2e05dc8 Add persistent navigation drawer to layout
Introduces a navigation drawer that can be toggled and persists its state in localStorage. The drawer displays route items and adapts its location and permanence based on device type. Also refactors initial drawer visibility and updates the app bar navigation icon to toggle the drawer.
2025-11-09 20:15:07 +01:00
2b139495a3 feat: update layout and routing structure with theme toggle functionality 2025-11-09 13:11:59 +01:00
7c227d45bb fix 2025-11-08 13:09:16 +01:00
e12795bacd chore: remove unnecessary Visual Studio database files 2025-10-21 20:38:44 +02:00
75393940b4 feat: add app bar with theme toggle and remove weather forecast demo code 2025-10-19 21:28:11 +02:00
74cfa283a1 Add basic routing and layout structure
Replaced App.vue with Layout.vue to serve as the main layout. Added Home and 404NotFound route components and configured router to handle home and fallback routes. Updated main.ts to use the new layout component.
2025-10-04 16:43:09 +02:00
9ccfd2e526 Remove GUI README documentation
Deleted the GUI/README.md file, removing setup and usage instructions for the Vue 3 project. This may be part of a cleanup or migration effort.
2025-10-04 16:26:50 +02:00
59 changed files with 1090 additions and 132 deletions

14
.gitignore vendored
View File

@@ -67,10 +67,22 @@ yarn-error.log*
# Editor / IDE
.vscode/
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# JetBrains IDEs
.idea/
.idea/
# Claude Code
.claude
# Database (Sql Lite)
API/app.db-wal
API/app.db-shm
# Vite build output
GUI/dist/
wwwroot/

Binary file not shown.

View File

@@ -1,12 +1,49 @@
{
"Version": 1,
"WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\",
"Documents": [],
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|d:\\programmieren\\csharp\\judoweb\\api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|solutionrelative:api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{0174dea2-fdbe-4ef1-8f99-c0beae78880f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "Program.cs",
"DocumentMoniker": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeDocumentMoniker": "API\\Program.cs",
"ToolTip": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeToolTip": "API\\Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-10-05T16:26:39.39Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
}
]
}

View File

@@ -1,12 +1,49 @@
{
"Version": 1,
"WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\",
"Documents": [],
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|d:\\programmieren\\csharp\\judoweb\\api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|solutionrelative:api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{0174dea2-fdbe-4ef1-8f99-c0beae78880f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "Program.cs",
"DocumentMoniker": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeDocumentMoniker": "API\\Program.cs",
"ToolTip": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeToolTip": "API\\Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-10-05T16:26:39.39Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
}
]
}

View File

@@ -1,13 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<Compile Remove="Services\Interfaces\**" />
<Content Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<Content Remove="Services\Interfaces\**" />
<EmbeddedResource Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<EmbeddedResource Remove="Services\Interfaces\**" />
<None Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<None Remove="Services\Interfaces\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Ulid" Version="1.4.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,73 @@
using API.Models.Ingoing.Altersgruppen;
using API.Repository.AgeGroup;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("api/ageGroups/")]
public class AgeGroupController : ControllerBase
{
private IAgeGroupService _ageGroupService;
public AgeGroupController(IAgeGroupService ageGroupService)
{
_ageGroupService = ageGroupService;
}
[HttpGet()]
public async Task<IActionResult> GetAll()
{
var allAgeGroups = await _ageGroupService.GetAllAsync();
return Ok(allAgeGroups);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOne([FromRoute] string id)
{
var group = await _ageGroupService.GetAsync(id);
if (group == null)
{
return NotFound();
}
return Ok(group);
}
[HttpPost()]
public async Task<IActionResult> Create([FromBody] AltersGruppeIngoing groupDto)
{
var group = await _ageGroupService.CreateAsync(groupDto.ToInternalFromIngoing());
return CreatedAtAction(nameof(GetOne), new { Id = group.Id }, group);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update([FromRoute] string id, [FromBody] AltersGruppeIngoing groupDto)
{
var group = await _ageGroupService.UpdateAsync(id, groupDto.ToInternalFromIngoing());
if(group == null)
{
return NotFound();
}
return Ok(group);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromRoute] string Id)
{
var group = await _ageGroupService.DeleteAsync(Id);
if (group == null)
{
return NotFound();
}
return NoContent();
}
}
}

View File

@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@@ -0,0 +1,21 @@
using API.Models.Internal.Altersgruppen;
using Microsoft.EntityFrameworkCore;
namespace API.Database
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<AltersGruppe> Altersgruppen { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}
}

View File

@@ -0,0 +1,34 @@
using API.Models.Internal.Altersgruppen;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace API.Database.Configurations
{
public class AltersGruppeConfiguration : IEntityTypeConfiguration<AltersGruppe>
{
public void Configure(EntityTypeBuilder<AltersGruppe> entity)
{
// Primary Key
entity.HasKey(e => e.Id);
// Id-Konfiguration
entity.Property(e => e.Id)
.HasMaxLength(26)
.IsRequired()
.ValueGeneratedNever();
// Name
entity.Property(e => e.Name)
.HasMaxLength(100)
.IsRequired();
// StartingAge
entity.Property(e => e.StartingAge)
.IsRequired();
// EndingAge
entity.Property(e => e.EndingAge)
.IsRequired();
}
}
}

View File

@@ -0,0 +1,46 @@
// <auto-generated />
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251207192702_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
modelBuilder.Entity("API.Models.Internal.Altersgruppen.AltersGruppe", b =>
{
b.Property<string>("Id")
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<int>("EndingAge")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("StartingAge")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Altersgruppen");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Altersgruppen",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 26, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
StartingAge = table.Column<int>(type: "INTEGER", nullable: false),
EndingAge = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Altersgruppen", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Altersgruppen");
}
}
}

View File

@@ -0,0 +1,43 @@
// <auto-generated />
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
modelBuilder.Entity("API.Models.Internal.Altersgruppen.AltersGruppe", b =>
{
b.Property<string>("Id")
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<int>("EndingAge")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("StartingAge")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Altersgruppen");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,28 @@
using API.Models.Internal.Altersgruppen;
using System.ComponentModel.DataAnnotations;
namespace API.Models.Ingoing.Altersgruppen
{
public class AltersGruppeIngoing
{
[Required]
public string Name { get; set; }
[Required]
public int StartingAge { get; set; }
[Required]
public int EndingAge { get; set; }
}
public static class AltersgruppeMapper
{
public static AltersGruppe ToInternalFromIngoing(this AltersGruppeIngoing group)
{
return new AltersGruppe
{
Name = group.Name,
StartingAge = group.StartingAge,
EndingAge = group.EndingAge,
};
}
}
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace API.Models.Internal.Altersgruppen
{
public class AltersGruppe
{
public string Id { get; set; }
public string Name { get; set; }
public int StartingAge { get; set; }
public int EndingAge { get; set; }
// Constructor für automatische ULID-Generierung
public AltersGruppe()
{
Id = Ulid.NewUlid().ToString();
}
}
}

View File

@@ -1,12 +1,39 @@
var builder = WebApplication.CreateBuilder(args);
using API.Database;
using API.Repository.AgeGroup;
using Microsoft.EntityFrameworkCore;
// Add services to the container.
var webRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "wwwroot"));
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
WebRootPath = webRoot
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Database
var postgreConnection = builder.Configuration.GetConnectionString("PostgresConnection");
if (!string.IsNullOrEmpty(postgreConnection))
{
// Nutze PostgresSQL
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(postgreConnection));
}
else
{
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite("Data Source=app.db"));
}
// Adding Database Services
builder.Services.AddScoped<IAgeGroupService, AgeGroupService>();
// Adding S3 Services
// Adding (latler) Redis Services
// Add Database Services
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -16,10 +43,22 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
using(var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate();
}
// Deliver frontend
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// Frontend Fallback
app.MapFallbackToFile("index.html");
app.Run();

View File

@@ -0,0 +1,68 @@
using API.Database;
using API.Models.Internal.Altersgruppen;
using Microsoft.EntityFrameworkCore;
namespace API.Repository.AgeGroup
{
public class AgeGroupService : IAgeGroupService
{
private ApplicationDbContext _context;
public AgeGroupService(ApplicationDbContext context)
{
_context = context;
}
public async Task<AltersGruppe> CreateAsync(AltersGruppe altersGruppe)
{
await _context.Altersgruppen.AddAsync(altersGruppe);
await _context.SaveChangesAsync();
return altersGruppe;
}
public async Task<AltersGruppe?> DeleteAsync(string id)
{
var group = await _context.Altersgruppen.FirstOrDefaultAsync(x => x.Id == id);
if (group == null)
{
return null;
}
_context.Altersgruppen.Remove(group);
_context.SaveChanges();
return group;
}
public async Task<List<AltersGruppe>> GetAllAsync()
{
var allGroups = await _context.Altersgruppen.ToListAsync();
return allGroups;
}
public async Task<AltersGruppe?> GetAsync(string id)
{
return await _context.Altersgruppen.FindAsync(id);
}
public async Task<AltersGruppe?> UpdateAsync(string id, AltersGruppe altersGruppe)
{
var existingGroup = await _context.Altersgruppen.FirstOrDefaultAsync(x => x.Id == id);
if (existingGroup == null)
{
return null;
}
existingGroup.Name = altersGruppe.Name;
existingGroup.StartingAge = altersGruppe.StartingAge;
existingGroup.EndingAge = altersGruppe.EndingAge;
await _context.SaveChangesAsync();
return existingGroup;
}
}
}

View File

@@ -0,0 +1,14 @@
using API.Models.Internal.Altersgruppen;
namespace API.Repository.AgeGroup
{
public interface IAgeGroupService
{
public Task<List<AltersGruppe>> GetAllAsync();
public Task<AltersGruppe?> GetAsync(string id);
public Task<AltersGruppe> CreateAsync(AltersGruppe altersGruppe);
public Task<AltersGruppe?> DeleteAsync(string id);
public Task<AltersGruppe?> UpdateAsync(string id, AltersGruppe altersGruppe);
}
}

View File

@@ -1,13 +0,0 @@
namespace API
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

BIN
API/app.db Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"semi": true,
"singleQuote": true,
"printWidth": 100
}

View File

@@ -1,48 +0,0 @@
# ./
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

View File

@@ -4,10 +4,10 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>Judoteam - Stadtlohn</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@@ -1,11 +0,0 @@
<script setup lang="ts"></script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
</template>
<style scoped></style>

94
GUI/src/Layout.vue Normal file
View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useTheme } from 'vuetify';
import { Visibility, routes } from './plugins/routesLayout';
import { useRoute } from 'vue-router';
const theme = useTheme();
const route = useRoute();
const showDrawer = ref(true);
function changeTheme() {
theme.toggle();
localStorage.setItem('theme', theme.name.value);
}
function toggleDrawer() {
showDrawer.value = !showDrawer.value;
localStorage.setItem('drawer', showDrawer.value ? 'Y' : 'N');
}
onMounted(() => {
theme.change(
localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'),
);
showDrawer.value = localStorage.getItem('drawer')?.startsWith('Y') || false;
});
</script>
<template>
<v-app>
<v-app-bar image="/static/images/appBarIcon.png">
<template v-slot:image>
<v-img
:gradient="
theme.name.value === 'dark'
? 'rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)'
: 'rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)'
"
/>
</template>
<template v-slot:prepend>
<v-app-bar-nav-icon @click="toggleDrawer()"></v-app-bar-nav-icon>
</template>
<v-app-bar-title class="title" @click="$router.push({ name: 'Home' })"
><span class="pointer">Judoteam - Stadtlohn</span></v-app-bar-title
>
<v-tooltip v-if="!$vuetify.display.mobile">
<template #activator="{ props }">
<v-btn icon v-bind="props">
<v-icon>mdi-account</v-icon>
</v-btn>
</template>
Account
</v-tooltip>
<v-tooltip>
<template #activator="{ props }">
<v-btn icon @click="changeTheme()" v-bind="props">
<v-icon>mdi-brightness-6</v-icon>
</v-btn>
</template>
{{ theme.name.value === 'dark' ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren' }}
</v-tooltip>
</v-app-bar>
<v-navigation-drawer
v-model="showDrawer"
:location="$vuetify.display.mobile ? 'bottom' : undefined"
:permanent="!$vuetify.display.mobile"
>
<v-list>
<v-list-item
v-for="item in routes.filter((x) => x.visible === Visibility.Public)"
:key="item.path"
:to="item.path"
:active="route.path === item.path"
link
:prepend-icon="item.icon"
:title="item.name"
class="rounded-lg mr-1 ml-1"
>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
<style scoped></style>

View File

@@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '../App.vue'
describe('App', () => {
it('mounts renders properly', () => {
const wrapper = mount(App)
expect(wrapper.text()).toContain('You did it!')
})
})

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
const props = defineProps({
image: String,
text: String,
noCover: Boolean,
})
</script>
<template>
<v-carousel-item :src="props.image" :cover="!props.noCover">
<template #default>
<div class="text-center">
<p class="text-h CarouselWithTitleText">{{ text }}</p>
</div>
</template>
</v-carousel-item>
</template>
<style scoped>
.CarouselWithTitleText{
background-color: rgb(0, 0, 0, 0.65);
color: white;
display: inline-block;
padding: 2px;
padding-right: 10px;
padding-left: 10px;
border-radius: 0px 0px 8px 8px;
}
</style>

View File

@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
title: String,
noMarginTop: Boolean,
extraBesideText: {
type: Boolean,
default: false,
},
extraWidthPercent: {
type: Number,
default: 30,
},
titleInSplitRight: {
type: Boolean,
default: false,
},
});
const normalizedExtraWidth = computed(() => {
const width = Number.isFinite(props.extraWidthPercent) ? props.extraWidthPercent : 30;
return Math.min(100, Math.max(0, width));
});
const textWidthPercent = computed(() => 100 - normalizedExtraWidth.value);
const extraWidthStyle = computed(() => ({
flexBasis: `${normalizedExtraWidth.value}%`,
maxWidth: `${normalizedExtraWidth.value}%`,
}));
const textWidthStyle = computed(() => ({
flexBasis: `${textWidthPercent.value}%`,
maxWidth: `${textWidthPercent.value}%`,
}));
</script>
<template>
<div :class="{ 'mt-6': !noMarginTop }">
<template v-if="props.extraBesideText">
<h3 v-if="!props.titleInSplitRight" class="text-h4 font-weight-bold">
{{ props.title }}
</h3>
<div class="entry-content entry-content--split">
<h3
v-if="props.titleInSplitRight"
class="text-h4 font-weight-bold entry-content__title--mobile"
>
{{ props.title }}
</h3>
<div class="entry-content__extra" :style="extraWidthStyle">
<slot name="extra" />
</div>
<div class="entry-content__text" :style="textWidthStyle">
<h3
v-if="props.titleInSplitRight"
class="text-h4 font-weight-bold entry-content__title--desktop"
>
{{ props.title }}
</h3>
<p class="text-subtitle-1">
<slot name="text" />
</p>
</div>
</div>
</template>
<template v-else>
<h3 class="text-h4 font-weight-bold">{{ props.title }}</h3>
<p class="text-subtitle-1">
<slot name="text" />
</p>
<slot name="extra" />
</template>
</div>
</template>
<style scoped>
.entry-content {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.entry-content--split {
align-items: center;
}
.entry-content__extra,
.entry-content__text {
min-width: 0;
}
.entry-content__extra {
flex: 0 0 auto;
}
.entry-content__text {
flex: 1 1 auto;
}
/* Desktop/Mobile title visibility helpers */
.entry-content__title--mobile {
display: none;
}
.entry-content__title--desktop {
display: block;
}
@media (max-width: 600px) {
.entry-content--split {
flex-direction: column;
align-items: stretch;
}
.entry-content__extra,
.entry-content__text {
flex-basis: auto !important;
max-width: 100% !important;
width: 100%;
}
.entry-content__title--mobile {
display: block;
}
.entry-content__title--desktop {
display: none;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import HomeEntrie from '@/components/HomeEntrie.vue';
const props = defineProps({
title: String,
image: String,
});
</script>
<template>
<home-entrie
:title="props.title"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
<slot name="text"></slot>
</template>
<template #extra>
<v-img class="rounded-lg" :src="props.image" height="400" cover> </v-img>
</template>
</home-entrie>
</template>
<style scoped>
.paddingIn {
padding-left: 75px;
padding-right: 75px;
}
</style>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useDisplay } from 'vuetify';
export interface TabItem {
value: string;
label: string;
}
defineProps<{
items: TabItem[];
tabColor?: string;
}>();
const display = useDisplay();
const tab = ref('1');
</script>
<template>
<!-- Desktop: Tabs mit einzelner Anzeige -->
<template v-if="!display.mobile.value">
<v-tabs :color="tabColor ?? '#b62b2b'" v-model="tab">
<v-tab v-for="item in items" :key="item.value" :value="item.value">
{{ item.label }}
</v-tab>
</v-tabs>
<v-divider></v-divider>
<v-tabs-window v-model="tab">
<v-tabs-window-item v-for="item in items" :key="item.value" :value="item.value">
<slot :name="item.value"></slot>
</v-tabs-window-item>
</v-tabs-window>
</template>
<!-- Mobile: Alle Einträge untereinander -->
<template v-else>
<template v-for="item in items" :key="item.value">
<slot :name="item.value"></slot>
</template>
</template>
</template>

View File

@@ -0,0 +1,8 @@
.title {
font-weight: 600;
}
.pointer{
cursor: pointer;
user-select: none;
}

View File

@@ -1,5 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import App from './Layout.vue'
import router from './router'
import vuetify from './plugins/vuetify'

View File

View File

@@ -0,0 +1,42 @@
import Home from '@/routes/Home.vue'
import NotFound from '@/routes/404NotFound.vue'
import type { RouteRecordRaw } from 'vue-router'
export enum Visibility {
Hidden,
Authorized,
Public
}
export interface LayoutRoute {
path: string,
name: string,
description: string,
icon: string,
visible: Visibility,
meta: RouteRecordRaw
}
export const routes: LayoutRoute[] = [
{
path: "/",
name: "Startseite",
description: "Übersicht der Anwendung",
icon: "mdi-home",
visible: Visibility.Public,
meta: {
name: 'Home',
path: '/',
component: Home
}
},
{
path: "/notFound",
name: "Nicht Verfügbar",
description: "Diese Seite wurde nicht gefunden",
icon: "mdi-information-outline",
visible: Visibility.Hidden,
meta: { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
}
]

View File

@@ -1,8 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from '@/plugins/routesLayout'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
routes: routes.map(x => x.meta)
})
export default router

View File

@@ -0,0 +1,21 @@
<script setup lang="ts"></script>
<template>
<h1>404</h1>
</template>
<style scoped>
.error-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
letter-spacing: 2px;
color: #fff;
}
.error-message {
font-size: 1.25rem;
opacity: 0.85;
color: #fff;
}
</style>

143
GUI/src/routes/Home.vue Normal file
View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import CarouselItemWithTitle from '@/components/CarouselItemWithTitle.vue';
import HomeEntrie from '@/components/HomeEntrie.vue';
import HomeEntrieWithImagePreset from '@/components/HomeEntrieWithImagePreset.vue';
import ResponsiveTabsOrList from '@/components/ResponsiveTabsOrList.vue';
const tabItems = [
{ value: '1', label: 'Nikolausturnier' },
{ value: '2', label: 'Wettkämpfe' },
{ value: '3', label: 'Kinoabende' },
];
</script>
<template>
<v-container fluid>
<v-img class="rounded-lg" height="40vh" src="/static/images/startPage.png" cover />
<main>
<v-container
id="main"
:style="{
maxWidth: $vuetify.display.smAndDown ? '100vw' : '70vw',
}"
fluid
>
<home-entrie title="Herzlich Willkommen!">
<template #text>
<b>Schön, dass du den Weg zu uns gefunden hast!</b> Egal, ob du schon lange dabei bist
oder das erste Mal von Judo hörst wir freuen uns, dich bald (wieder) in unserem
<b>Judozentrum</b>
(Dojo) begrüßen zu dürfen.
</template>
</home-entrie>
<home-entrie
title="Was ist Judo?"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
Judo ist eine Sportart, welche den ganzen Körper fordert, fördert und beansprucht. Es
werden also <b>sämtliche Muskelgruppen, sowie das Köpfchen bei uns trainiert. </b>
<b>Judo bedeutet übersetzt der sanfte Weg.</b> Daraus lässt sich ja schon erahnen,
dass Judo eine Kampfsportart ist bei der das "Schlagen" oder "Treten" des Gegners
verboten ist. Beim Judo beschränkt man sich rein auf das <b>Werfen</b>,
<b>fixieren am Boden</b>, <b>Hebeln</b> und <b>Hebeln</b> (nur für die älteren) des
Partners/Gegners.
</template>
<template #extra>
<v-carousel
show-arrows="hover"
hide-delimiter-background
class="rounded-lg"
cycle
interval="8000"
>
<carousel-item-with-title text="Werfen" image="/static/images/whatsJudo/throw.jpg" />
<carousel-item-with-title
text="fixieren am Boden"
image="/static/images/whatsJudo/hold.jpg"
/>
<carousel-item-with-title
text="Trainieren sämtlicher Muskelgruppen"
image="/static/images/whatsJudo/stabil.jpg"
/>
</v-carousel>
</template>
</home-entrie>
<home-entrie
title="Unsere Werte beim Judo"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
Zu dem Spielen beim Judo die Werte wie <b>Respekt</b>, <b>Disziplin</b> und
<b>Selbstbehauptung</b> eine große Rolle und so werden auch diese Werte bei uns
gefördert.
</template>
<template #extra>
<v-timeline align="start" side="end">
<v-timeline-item dot-color="blue">
<h6 class="text-h6">Respekt</h6>
</v-timeline-item>
<v-timeline-item dot-color="green">
<h6 class="text-h6">Selbstbehauptung</h6>
</v-timeline-item>
<v-timeline-item dot-color="orange">
<h6 class="text-h6">Disziplin</h6>
</v-timeline-item>
</v-timeline>
</template>
</home-entrie>
<home-entrie title="Und sonst so?"></home-entrie>
<responsive-tabs-or-list :items="tabItems">
<template #1>
<home-entrie-with-image-preset
title="Nikolausturnier"
image="/static/images/nikolausturnier/total.png"
>
<template #text>
Ein Highlight im Jahreskalender ist unser alljährliches Nikolausturnier, bei dem
Judoka aller Altersgruppen zusammenkommen, um ihre Fähigkeiten zu messen und
gemeinsam Spaß zu haben.
</template>
</home-entrie-with-image-preset>
</template>
<template #2>
<home-entrie-with-image-preset
title="Wettkämpfe"
image="/static/images/wettkampf/total.jpg"
>
<template #text>
Für diejenigen, die sich gerne messen wollen, bieten wir die Teilnahme an
Wettkämpfen auf verschiedenen Ebenen an von lokalen Turnieren bis hin zu
nationalen Meisterschaften.
</template>
</home-entrie-with-image-preset>
</template>
<template #3>
<home-entrie-with-image-preset
title="Kinoabende"
image="/static/images/kinoabend/total.jpg"
>
<template #text>
Bei unseren Kinoabenden schauen wir in lockerer Runde ausgewählte Filme nicht nur
judobezogene, sondern auch beliebte Klassiker und aktuelle Hits. Gemeinsam
entspannen, Snacks teilen und über Filme und Sport plaudern.
</template>
</home-entrie-with-image-preset>
</template>
</responsive-tabs-or-list>
</v-container>
</main>
</v-container>
</template>
<style scoped></style>

View File

@@ -15,4 +15,7 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
build: {
outDir: "../wwwroot"
}
})

View File

@@ -1 +1 @@
# JudoWeb
# JudoWebsurf