Add reusable UI components and toast notification system

- Introduce ActionPanel, EnvelopeInfoCard, TfaForm, ConfirmDialog, StatusPage, and Toast components for modular, presentational UI
- Add ToastService for pub/sub toast notifications; register in DI
- Refactor AccessCodeForm for improved UX and parameterization
- Enhance MainLayout with Toast integration and better error handling
- Standardize and extend app.css for new components and responsive design
- All new components are "dumb" (no service/API knowledge), using EventCallbacks for parent interaction
- ConfirmDialog supports awaitable user confirmation via TaskCompletionSource
This commit is contained in:
OlgunR
2026-03-23 12:37:14 +01:00
parent 0a544cfe85
commit 7aa9853756
11 changed files with 1109 additions and 117 deletions

View File

@@ -0,0 +1,86 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Service für Toast-Benachrichtigungen. Ersetzt AlertifyJS aus dem Web-Projekt.
///
/// WARUM ein Service und keine Komponente mit Parametern?
/// - Toasts können von ÜBERALL ausgelöst werden (Pages, Services, andere Komponenten)
/// - Ein Service ist über Dependency Injection überall verfügbar
/// - Die Toast-Komponente im Layout hört auf diesen Service und rendert die Nachrichten
///
/// PATTERN: Pub/Sub (Publisher/Subscriber)
/// - Publisher: Jede Komponente die _toast.ShowSuccess("...") aufruft
/// - Subscriber: Die Toast-Komponente im MainLayout, die auf OnChange hört
///
/// Das ist das gleiche Pattern wie beim EnvelopeState.
/// </summary>
public class ToastService
{
/// <summary>
/// Liste aller aktuell sichtbaren Toasts.
/// Mehrere Toasts können gleichzeitig angezeigt werden (gestapelt).
/// </summary>
public List<ToastMessage> Messages { get; } = [];
/// <summary>Event: Informiert die Toast-Komponente über Änderungen</summary>
public event Action? OnChange;
public void ShowSuccess(string text) => Show(text, "success");
public void ShowError(string text) => Show(text, "danger");
public void ShowInfo(string text) => Show(text, "info");
public void ShowWarning(string text) => Show(text, "warning");
/// <summary>
/// Fügt einen Toast hinzu und entfernt ihn nach der angegebenen Dauer automatisch.
///
/// WARUM async void?
/// Normalerweise vermeidet man async void. Hier ist es ok, weil:
/// - Es ist ein Fire-and-Forget-Timer (wir warten nicht auf das Ergebnis)
/// - Fehler im Delay können die App nicht zum Absturz bringen
/// - Das ist ein gängiges Pattern für Auto-Dismiss-Logik
/// </summary>
private async void Show(string text, string type, int durationMs = 4000)
{
var message = new ToastMessage(text, type);
Messages.Add(message);
OnChange?.Invoke();
// Nach Ablauf der Dauer automatisch entfernen
await Task.Delay(durationMs);
Messages.Remove(message);
OnChange?.Invoke();
}
/// <summary>Entfernt einen Toast sofort (z.B. wenn der Benutzer auf X klickt)</summary>
public void Dismiss(ToastMessage message)
{
Messages.Remove(message);
OnChange?.Invoke();
}
}
/// <summary>
/// Ein einzelner Toast-Eintrag.
///
/// WARUM ein record statt class?
/// - Records haben automatisch Equals/GetHashCode basierend auf allen Properties
/// - Wir brauchen das für Messages.Remove() — es vergleicht über Referenz-Gleichheit
/// - Die Id (Guid) macht jeden Toast einzigartig, auch bei gleichem Text
/// </summary>
public record ToastMessage(string Text, string Type)
{
/// <summary>Eindeutige Id — damit zwei Toasts mit gleichem Text unterscheidbar sind</summary>
public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// Gibt die Bootstrap-Icon-Klasse basierend auf dem Typ zurück.
/// success → check-circle, danger → x-circle, etc.
/// </summary>
public string IconClass => Type switch
{
"success" => "bi-check-circle-fill",
"danger" => "bi-x-circle-fill",
"warning" => "bi-exclamation-triangle-fill",
_ => "bi-info-circle-fill"
};
}