Files
Module.HallOfFame/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Edit.razor

383 lines
15 KiB
Plaintext

@using Oqtane.Modules.Controls
@using SZUAbsolventenverein.Module.HallOfFame.Services
@using SZUAbsolventenverein.Module.HallOfFame.Models
@using Microsoft.AspNetCore.Components.Forms
@namespace SZUAbsolventenverein.Module.HallOfFame
@implements IDisposable
@inherits ModuleBase
@inject IHallOfFameService HallOfFameService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject ISettingService SettingService
<form @ref="form" class="@(validated ? " was-validated" : "needs-validation" )" novalidate>
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
<Label For="name" HelpText="Gib deinen Namen ein" ResourceKey="Name">Name:</Label>
</div>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required maxlength="120" />
<div class="invalid-feedback">Bitte gib einen Namen ein (max. 120 Zeichen).</div>
</div>
</div>
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
<Label For="year" HelpText="Gib das Jahr ein, in dem du die Matura abgeschlossen hast (z.B. 2020)"
ResourceKey="Year">Jahrgang: </Label>
</div>
<div class="col-sm-9">
<input id="year" type="number" class="form-control" @bind="@_year" required min="1900"
max="@Int32.MaxValue" />
<div class="invalid-feedback">Bitte gib einen gültigen Jahrgang ein.</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<Label For="description" HelpText="Kurzbeschreibung / Werdegang" ResourceKey="Description">Beschreibung:
</Label>
</div>
<div class="col-sm-9">
@if (_descriptionLoaded)
{
<RichTextEditor Content="@_description" @ref="@_richTextEditorRef"
Placeholder="Beschreibe deinen Werdegang..."></RichTextEditor>
<div class="text-muted small mt-1">
Aktuell: <strong
class="@(_currentCharCount > _charLimit ? "text-danger" : "text-success")">@_currentCharCount</strong>
von maximal @_charLimit Zeichen.
</div>
}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3">
<Label For="image" HelpText="Porträtfoto hochladen (JPG/PNG)" ResourceKey="Image">Foto: </Label>
</div>
<div class="col-sm-9">
<div class="mb-2">
@if (!string.IsNullOrEmpty(_image))
{
<div class="position-relative d-inline-block">
<img src="@_image"
style="max-height: 150px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ddd;"
alt="Vorschau" />
<button type="button" class="btn btn-sm btn-danger"
style="position: absolute; top: 5px; right: 5px;" @onclick="RemoveImage"
title="Bild entfernen">
<i class="oi oi-x"></i>
</button>
</div>
}
else
{
<div class="bg-light d-flex align-items-center justify-content-center text-muted"
style="height: 150px; width: 150px; border: 2px dashed #ddd; border-radius: 8px;">
Kein Bild
</div>
}
</div>
<InputFile OnChange="HandleFileSelected" class="form-control" accept=".jpg,.jpeg,.png" />
<div class="text-muted small mt-1">Nur JPG oder PNG, max. 5 MB.</div>
@if (_uploading)
{
<div class="spinner-border spinner-border-sm text-primary mt-2" role="status">
<span class="visually-hidden">Wird hochgeladen...</span>
</div>
<span class="small text-primary ms-1">Wird hochgeladen...</span>
}
</div>
</div>
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
<Label For="link" HelpText="Externer Link (optional)" ResourceKey="Link">Link: </Label>
</div>
<div class="col-sm-9">
<input id="link" type="url" class="form-control" @bind="@_link" placeholder="https://" />
<div class="invalid-feedback">Bitte gib eine gültige URL ein (startet mit http:// oder https://).</div>
</div>
</div>
<div class="row mb-3 align-items-center">
<div class="col-sm-3">
<Label For="status" HelpText="Status" ResourceKey="Status">Status: </Label>
</div>
<div class="col-sm-9">
<p>Aktuell: <strong>@(_status ?? "Neu")</strong></p>
</div>
</div>
</div>
<div class="mt-4 d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-secondary me-2" @onclick="@(() => Save("Draft"))"
disabled="@_uploading">Als Entwurf speichern</button>
<button type="button" class="btn btn-primary" @onclick="@(() => Save("Published"))"
disabled="@_uploading">Veröffentlichen</button>
<NavLink class="btn btn-link ms-2" href="@NavigateUrl()">Abbrechen</NavLink>
</div>
@if (PageState.Action == "Edit")
{
<button type="button" class="btn btn-outline-danger" @onclick="DeleteEntry" disabled="@_uploading">
<i class="oi oi-trash me-1"></i> Eintrag löschen
</button>
}
</div>
<br /><br />
@if (PageState.Action == "Edit")
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon">
</AuditInfo>
}
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Actions => "Add,Edit";
public override string Title => "Hall of Fame Eintrag verwalten";
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet("_content/SZUAbsolventenverein.Module.HallOfFame/Module.css")
};
private ElementReference form;
private bool validated = false;
private bool _uploading = false;
private RichTextEditor _richTextEditorRef;
private bool _descriptionLoaded = false;
private int _id;
private string _name;
private int _year = DateTime.Now.Year;
private string _description;
private string _image;
private string _link;
private string _status = "Draft";
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private int _charLimit = 500;
private int _currentCharCount = 0;
private System.Timers.Timer _timer;
protected override async Task OnInitializedAsync()
{
_timer = new System.Timers.Timer(1000); // 1 sekunde
_timer.Elapsed += async (s, e) =>
{
if (_richTextEditorRef != null && _descriptionLoaded)
{
try
{
var html = await _richTextEditorRef.GetHtml();
var plainText = System.Text.RegularExpressions.Regex.Replace(html ?? "", "<.*?>", String.Empty);
plainText = System.Net.WebUtility.HtmlDecode(plainText);
if (_currentCharCount != plainText.Length)
{
_currentCharCount = plainText.Length;
await InvokeAsync(StateHasChanged);
}
}
catch { } // Ignore interop errors during disposal
}
};
_timer.Start();
try
{
// Load character limit setting
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
var charLimitStr = SettingService.GetSetting(settings, "CharLimit", "500");
if (int.TryParse(charLimitStr, out var parsed) && parsed >= 4 && parsed <= 32000)
{
_charLimit = parsed;
}
_descriptionLoaded = false;
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);
HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId);
if (HallOfFame != null)
{
if (HallOfFame.UserId != PageState.User.UserId)
{
NavigationManager.NavigateTo(NavigateUrl());
return;
}
_name = HallOfFame.Name;
_year = HallOfFame.Year;
_description = HallOfFame.Description;
_image = HallOfFame.Image;
_link = HallOfFame.Link;
_status = HallOfFame.Status;
_createdby = HallOfFame.CreatedBy;
_createdon = HallOfFame.CreatedOn;
_modifiedby = HallOfFame.ModifiedBy;
_modifiedon = HallOfFame.ModifiedOn;
}
}
else // Add Mode
{
var existing = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId);
if (existing != null)
{
NavigationManager.NavigateTo(EditUrl(existing.HallOfFameId.ToString()));
}
}
_descriptionLoaded = true;
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading HallOfFame {HallOfFameId} {Error}", _id, ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
var file = e.File;
if (file == null) return;
if (file.Size > 5 * 1024 * 1024)
{
AddModuleMessage("Die Datei ist zu groß (max. 5 MB allowed).", MessageType.Warning);
return;
}
try
{
_uploading = true;
using var stream = file.OpenReadStream(5 * 1024 * 1024);
var url = await HallOfFameService.UploadFileAsync(stream, file.Name, ModuleState.ModuleId);
if (!string.IsNullOrEmpty(url))
{
_image = url;
AddModuleMessage("Foto erfolgreich hochgeladen.", MessageType.Success);
}
else
{
AddModuleMessage("Fehler beim Hochladen des Fotos.", MessageType.Error);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Uploading File {Error}", ex.Message);
AddModuleMessage("Ein technischer Fehler ist beim Upload aufgetreten.", MessageType.Error);
}
finally
{
_uploading = false;
StateHasChanged();
}
}
private void RemoveImage()
{
_image = string.Empty;
StateHasChanged();
}
private async Task Save(string status)
{
try
{
ClearModuleMessage();
validated = true;
// Get the HTML content from the rich text editor
if (_richTextEditorRef != null)
{
_description = await _richTextEditorRef.GetHtml();
}
var interop = new Oqtane.UI.Interop(JSRuntime);
if (await interop.FormValid(form))
{
// Custom character limit validation for rich text
var plainText = System.Text.RegularExpressions.Regex.Replace(_description ?? "", "<.*?>", String.Empty);
plainText = System.Net.WebUtility.HtmlDecode(plainText);
if (plainText.Length > _charLimit)
{
AddModuleMessage($"Fehler: Die Beschreibung ist zu lang (Aktuell {plainText.Length}, Maximal {_charLimit} Zeichen).",
MessageType.Warning);
return;
}
_status = status;
if (PageState.Action == "Add")
{
HallOfFame HallOfFame = new HallOfFame();
HallOfFame.ModuleId = ModuleState.ModuleId;
HallOfFame.UserId = PageState.User.UserId;
HallOfFame.Name = _name;
HallOfFame.Year = _year;
HallOfFame.Description = _description;
HallOfFame.Image = _image;
HallOfFame.Link = _link;
HallOfFame.Status = _status;
HallOfFame = await HallOfFameService.AddHallOfFameAsync(HallOfFame);
await logger.LogInformation("HallOfFame Added {HallOfFame}", HallOfFame);
}
else
{
HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId);
if (HallOfFame.UserId == PageState.User.UserId)
{
HallOfFame.Name = _name;
HallOfFame.Year = _year;
HallOfFame.Description = _description;
HallOfFame.Image = _image;
HallOfFame.Link = _link;
HallOfFame.Status = _status;
await HallOfFameService.UpdateHallOfFameAsync(HallOfFame);
await logger.LogInformation("HallOfFame Updated {HallOfFame}", HallOfFame);
}
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.SaveValidation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving HallOfFame {Error}", ex.Message);
AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error);
}
}
private async Task DeleteEntry()
{
try
{
await HallOfFameService.DeleteHallOfFameAsync(_id, ModuleState.ModuleId);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting HallOfFame {Error}", ex.Message);
AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error);
}
}
public void Dispose()
{
_timer?.Stop();
_timer?.Dispose();
}
}