This commit is contained in:
Marc Wieland 2025-10-13 13:21:53 +02:00
commit 6bcf3e881b
1649 changed files with 75871 additions and 0 deletions

View File

@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -0,0 +1,16 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>

View File

@ -0,0 +1,77 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@ -0,0 +1,29 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">OnProfNext.Client</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.9" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OnProfNext.Shared\OnProfNext.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,71 @@
@page "/createuser"
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models
@inject UserApiService UserService
@inject NavigationManager Nav
<h3>Neuen Benutzer anlegen</h3>
@if (errorMessage is not null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>Fehler:</strong> @errorMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<EditForm Model="newUser" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label>Username</label>
<InputText class="form-control" @bind-Value="newUser.Username" />
</div>
<div class="mb-3">
<label>E-Mail</label>
<InputText class="form-control" @bind-Value="newUser.Email" />
</div>
<div class="mb-3">
<label>Passwort</label>
<InputText type="password" class="form-control" @bind-Value="newUser.PasswordHash" />
</div>
<div class="mb-3">
<label>Vorname</label>
<InputText class="form-control" @bind-Value="newUser.FirstName" />
</div>
<div class="mb-3">
<label>Nachname</label>
<InputText class="form-control" @bind-Value="newUser.LastName" />
</div>
<button class="btn btn-primary" type="submit">Speichern</button>
</EditForm>
@code {
private User newUser = new();
private string? errorMessage;
private async Task HandleValidSubmit()
{
var result = await UserService.CreateUserAsync(newUser);
if(!result.Success)
{
errorMessage = result.Error;
return;
}
else
{
Nav.NavigateTo("/users");
}
}
}

View File

@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -0,0 +1,263 @@
@page "/users"
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models
@inject UserApiService UserService
<h3 class="mb-3">Benutzerübersicht</h3>
@if (errorMessage is not null)
{
<div class="alert alert-danger">@errorMessage</div>
}
else if (users is null)
{
<p><em>Lade Daten...</em></p>
}
else if (!users.Any())
{
<p>Keine Benutzer gefunden.</p>
}
else
{
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Vorname</th>
<th>Nachname</th>
<th>E-Mail</th>
<th>Mandant</th>
</tr>
</thead>
<tbody>
@foreach (var u in users)
{
<tr class="user-row" @onclick="() => OpenUserDetail(u.Id)">
<td>@u.Username</td>
<td>@u.FirstName</td>
<td>@u.LastName</td>
<td>@u.Email</td>
<td>@u.MandantId</td>
</tr>
}
</tbody>
</table>
}
@if (selectedUser is not null)
{
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Benutzer bearbeiten</h5>
<button type="button" class="btn-close" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
<EditForm Model="selectedUser" OnValidSubmit="UpdateUserAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label>Vorname</label>
<InputText class="form-control" @bind-Value="selectedUser.FirstName" />
</div>
<div class="mb-3">
<label>Nachname</label>
<InputText class="form-control" @bind-Value="selectedUser.LastName" />
</div>
<div class="mb-3">
<label>E-Mail</label>
<InputText class="form-control" @bind-Value="selectedUser.Email" />
</div>
<div class="mb-3">
<label>Mandant</label>
<InputNumber class="form-control" @bind-Value="selectedUser.MandantId" />
</div>
<div class="form-check">
<InputCheckbox class="form-check-input" @bind-Value="selectedUser.IsActive" />
<label class="form-check-label">Aktiv</label>
</div>
<div class="modal-footer mt-3 d-flex justify-content-between">
<button type="button" class="btn btn-secondary" @onclick="CloseModal">Abbrechen</button>
<div>
<button type="submit" class="btn btn-primary me-2">Speichern</button>
<button type="button" class="btn btn-outline-danger" @onclick="ShowDeleteConfirm">Löschen</button>
</div>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@if (showDeleteConfirm && selectedUser is not null)
{
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.6);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">Benutzer löschen</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseDeleteConfirm"></button>
</div>
<div class="modal-body">
<p>
⚠️ Bist du sicher, dass du den Benutzer
<strong>@selectedUser.Username</strong> löschen möchtest?<br />
Dies kann <strong>nicht rückgängig gemacht</strong> werden.
</p>
<div class="mb-3">
<label class="form-label">Zur Bestätigung den Benutzernamen eingeben:</label>
<InputText class="form-control"
@bind-Value="deleteConfirmInput"
oninput="@((ChangeEventArgs e) => OnDeleteConfirmInputChanged(e))" />
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="CloseDeleteConfirm">Abbrechen</button>
<button class="btn"
style="@($"background-color: {(IsDeleteConfirmed ? "#dc3545" : "#f5c6cb")}; color: white;")"
disabled="@(IsDeleteConfirmed ? null : true)"
@onclick="ConfirmDeleteUser">
@(IsDeleteConfirmed ? "Endgültig löschen" : "Löschen")
</button>
</div>
</div>
</div>
</div>
}
@code {
private List<User>? users;
private string? errorMessage;
private User? selectedUser;
private bool showDeleteConfirm = false;
private string deleteConfirmInput = string.Empty;
private bool IsDeleteConfirmed => selectedUser is not null && deleteConfirmInput == selectedUser.Username;
protected override async Task OnInitializedAsync()
{
var result = await UserService.GetUsersAsync();
if (!result.Success)
{
errorMessage = result.Error;
return;
}
users = result.Data;
}
private void OpenUserDetail(int id)
{
selectedUser = users?.FirstOrDefault(u => u.Id == id);
}
private void CloseModal()
{
selectedUser = null;
showDeleteConfirm = false;
deleteConfirmInput = string.Empty;
}
private void OnDeleteConfirmInputChanged(ChangeEventArgs e)
{
deleteConfirmInput = e.Value?.ToString() ?? string.Empty;
StateHasChanged();
}
private void ShowDeleteConfirm()
{
showDeleteConfirm = true;
deleteConfirmInput = string.Empty;
}
private void CloseDeleteConfirm()
{
showDeleteConfirm = false;
deleteConfirmInput = string.Empty;
}
//Delete Action
private async Task ConfirmDeleteUser()
{
if(selectedUser is null)
{
return;
}
var result = await UserService.DeleteUserAsync(selectedUser.Id);
if(!result.Success)
{
errorMessage = result.Error;
return;
}
users?.Remove(selectedUser);
CloseModal();
StateHasChanged();
}
private async Task UpdateUserAsync()
{
if(selectedUser is null)
{
return;
}
var result = await UserService.UpdateUserAsync(selectedUser);
if(!result.Success)
{
errorMessage = result.Error;
return;
}
//Modal schließen und Liste aktualisieren
CloseModal();
var updated = await UserService.GetUsersAsync();
users = updated.Data;
StateHasChanged();
}
}
<style>
tr.user-row {
transition: all 0.2s ease-in-out;
cursor: pointer;
}
tr.user-row:hover {
transform: scale(1.01);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background-color: #f8f9fa;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
.modal-content {
border-radius: 0.75rem;
}
.btn-outline-danger:hover {
background-color: #dc3545;
color: #fff;
}
</style>

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using OnProfNext.Client;
using OnProfNext.Client.Services;
using Microsoft.Extensions.Http;
using System.Net.Http;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped<UserApiService>();
builder.Services.AddHttpClient<UserApiService>(client =>
{
client.BaseAddress = new Uri("https://localhost:7271/");
});
await builder.Build().RunAsync();

View File

@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5223",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7082;http://localhost:5223",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,95 @@
using OnProfNext.Shared.Models;
using System.Net.Http.Json;
namespace OnProfNext.Client.Services
{
public class UserApiService
{
private readonly HttpClient _httpClient;
public UserApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<(bool Success, List<User>? Data, string? Error)> GetUsersAsync()
{
try
{
var users = await _httpClient.GetFromJsonAsync<List<User>>("api/users");
return (true, users ?? new List<User>(), null);
}
catch (HttpRequestException ex)
{
return (false, null, "Keine Verbindung zum Server. Bitte später erneut versuchen.");
}
catch (Exception ex)
{
return(false, null, $"Unerwarteter Fehler: {ex.Message}");
}
}
public async Task<(bool Success, string? Error)> CreateUserAsync(User user)
{
try
{
var response = await _httpClient.PostAsJsonAsync("api/users", user);
if (!response.IsSuccessStatusCode)
{
return (false, $"Fehler beim Anlegen: {response.ReasonPhrase}");
}
return (true, null);
}
catch (HttpRequestException)
{
return (false, "Server nicht erreichbar.");
}
}
public async Task<(bool Success, string? Error)> UpdateUserAsync(User user)
{
try
{
var response = await _httpClient.PutAsJsonAsync($"api/users/{user.Id}", user);
if(!response.IsSuccessStatusCode)
{
return (false, $"Fehler beim Aktualisieren: {response.ReasonPhrase}");
}
return (true, null);
}
catch (HttpRequestException)
{
return (false, "Server nicht erreichbar.");
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
public async Task<(bool Success, string? Error)> DeleteUserAsync(int userId)
{
try
{
var response = await _httpClient.DeleteAsync($"api/users/{userId}");
if (!response.IsSuccessStatusCode)
{
return (false, $"Fehler beim Löschen: {response.ReasonPhrase}");
}
return (true, null);
}
catch (HttpRequestException)
{
return (false, "Server nicht erreichbar.");
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
}
}

View File

@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using OnProfNext.Client
@using OnProfNext.Client.Layout

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More