feat(halloffame): implement image upload and enhance module functionality

- Added image upload system (JPG/PNG, max 5MB) with live preview and removal option
- Fixed Concurrency Exception during deletion (split transactions for reports and entries)
- Optimized card layout: consistent height and height-based truncation for descriptions
- Added sort direction toggle (Ascending/Descending) with arrow icons for Date, Name, and Year
- Refactored HallOfFameService to use streams for Server/Wasm compatibility
- Improved error handling and UI feedback for upload and delete operations
This commit is contained in:
Adam Gaiswinkler
2026-02-10 17:45:48 +01:00
parent 2d8c6736a7
commit 1bff5ebbbd
18 changed files with 956 additions and 127 deletions

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
@@ -10,6 +11,8 @@ using SZUAbsolventenverein.Module.HallOfFame.Services;
using Oqtane.Controllers;
using System.Net;
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
{
@@ -17,10 +20,12 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
public class HallOfFameController : ModuleControllerBase
{
private readonly IHallOfFameService _HallOfFameService;
private readonly IWebHostEnvironment _environment;
public HallOfFameController(IHallOfFameService HallOfFameService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
public HallOfFameController(IHallOfFameService HallOfFameService, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor)
{
_HallOfFameService = HallOfFameService;
_environment = environment;
}
// GET: api/<controller>?moduleid=x
@@ -33,9 +38,10 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId);
// Filter: Show only Published unless user has Edit permissions (simplified check for now, can be expanded)
// For now, let's filter in memory or service. The requirement says: "Hauptseite zeigt nur Published".
// We will filter here.
if (User.IsInRole(RoleNames.Admin) || User.IsInRole(RoleNames.Host))
{
return list;
}
return list.Where(item => item.Status == "Published");
}
else
@@ -138,7 +144,47 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
return HallOfFame;
}
// DELETE api/<controller>/5
// PUT api/<controller>/report/5
[HttpPut("report/{id}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task Report(int id, [FromQuery] string reason)
{
Models.HallOfFame HallOfFame = await _HallOfFameService.GetHallOfFameAsync(id, -1);
if (HallOfFame != null && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId))
{
await _HallOfFameService.ReportAsync(id, HallOfFame.ModuleId, reason);
}
}
// GET api/<controller>/reports/5?moduleid=x
[HttpGet("reports/{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<IEnumerable<Models.HallOfFameReport>> GetReports(int id, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _HallOfFameService.GetHallOfFameReportsAsync(id, ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame GetReports Attempt {HallOfFameId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// DELETE api/<controller>/report/5/x
[HttpDelete("report/{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task DeleteReport(int id, int moduleid)
{
if (IsAuthorizedEntityId(EntityNames.Module, moduleid))
{
await _HallOfFameService.DeleteHallOfFameReportAsync(id, moduleid);
}
}
[HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task Delete(int id, int moduleid)
@@ -154,5 +200,33 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpPost("upload")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<IActionResult> Upload(IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest("Keine Datei ausgewählt.");
var extension = Path.GetExtension(file.FileName).ToLower();
if (extension != ".jpg" && extension != ".jpeg" && extension != ".png")
{
return BadRequest("Nur JPG und PNG Dateien sind erlaubt.");
}
var folder = Path.Combine(_environment.WebRootPath, "Content", "HallOfFame");
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
var fileName = Guid.NewGuid().ToString() + extension;
var path = Path.Combine(folder, fileName);
using (var stream = new FileStream(path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok(new { url = "/Content/HallOfFame/" + fileName });
}
}
}