- 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
86 lines
3.2 KiB
C#
86 lines
3.2 KiB
C#
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"
|
|
};
|
|
} |