From e436ee2754e93dd9629d59588ec0ced5aa6417d4 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 27 Dec 2025 09:11:40 +0100 Subject: [PATCH] Complete phase H8: Undo functionality (unbegrenzt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add UndoButton.razor as reusable component - Refactor GameInputPanel to use UndoButton component - Enhance GameEffects with proper UndoThrowAction handler - IState injection for state access in effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/IMPLEMENTATION_PLAN.md | 2 +- .../Components/Game/GameInputPanel.razor | 27 +---- .../Components/Game/UndoButton.razor | 103 ++++++++++++++++++ src/Koogle.Web/Store/GameState/GameEffects.cs | 30 ++++- 4 files changed, 132 insertions(+), 30 deletions(-) create mode 100644 src/Koogle.Web/Components/Game/UndoButton.razor diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 12cf7b6..f900393 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -955,7 +955,7 @@ Web: Store/GameState/, Components/Game/, Hubs/GameHub | ✓ | H5 | Games | Scheiss-Spiel + Trigger-Integration | 6 | | ✓ | H6 | UI | Game Setup Dialog | 4 | | ✓ | H7 | Integration | DayDetails Tabs | 3 | -| ☐ | H8 | Features | Undo (unbegrenzt, ohne Redo) | 2 | +| ✓ | H8 | Features | Undo (unbegrenzt, ohne Redo) | 2 | | ☐ | H9 | Persistenz | DB Persistence & Recovery | 4 | | ☐ | H9b | Sync | SignalR komplett + Auto-Reconnect | 5 | | ☐ | H10 | Testing | Kern-Logik Tests | 2 | diff --git a/src/Koogle.Web/Components/Game/GameInputPanel.razor b/src/Koogle.Web/Components/Game/GameInputPanel.razor index 91822f6..ffb622d 100644 --- a/src/Koogle.Web/Components/Game/GameInputPanel.razor +++ b/src/Koogle.Web/Components/Game/GameInputPanel.razor @@ -51,18 +51,9 @@ @* Undo button *@ - @if (GameState.Value.UndoStack.Count > 0) - { -
- - Letzten Wurf rückgängig (@GameState.Value.UndoStack.Count) - -
- } +
+ +
@* Error display *@ @if (!string.IsNullOrEmpty(GameState.Value.Error)) @@ -271,18 +262,6 @@ await OnThrowCompleted.InvokeAsync(result); } - private void HandleUndo() - { - if (GameState.Value.UndoStack.Count > 0) - { - var lastSnapshot = GameState.Value.UndoStack[^1]; - Dispatcher.Dispatch(new UndoThrowSuccessAction( - lastSnapshot.ThrowPanel, - lastSnapshot.Participants, - lastSnapshot.GameModel)); - } - } - private async Task ShowPlayerSelector() { await OnShowPlayerSelector.InvokeAsync(); diff --git a/src/Koogle.Web/Components/Game/UndoButton.razor b/src/Koogle.Web/Components/Game/UndoButton.razor new file mode 100644 index 0000000..e8519b3 --- /dev/null +++ b/src/Koogle.Web/Components/Game/UndoButton.razor @@ -0,0 +1,103 @@ +@using Koogle.Web.Store.GameState +@inherits FluxorComponent + +@inject IState GameState +@inject IDispatcher Dispatcher + +@* Only show when undo is available *@ +@if (CanUndo) +{ + + @ButtonText + +} + +@code { + /// + /// Whether to show the button even when no undo is available. + /// + [Parameter] + public bool AlwaysShow { get; set; } + + /// + /// Button variant (default: Outlined). + /// + [Parameter] + public Variant Variant { get; set; } = Variant.Outlined; + + /// + /// Whether button should be full width. + /// + [Parameter] + public bool FullWidth { get; set; } = true; + + /// + /// Button size. + /// + [Parameter] + public Size Size { get; set; } = Size.Medium; + + /// + /// Additional CSS classes. + /// + [Parameter] + public string? Class { get; set; } + + /// + /// Optional custom button text. If not set, shows count. + /// + [Parameter] + public string? CustomText { get; set; } + + /// + /// Whether to show the undo count in the button text. + /// + [Parameter] + public bool ShowCount { get; set; } = true; + + /// + /// Callback when undo is triggered. + /// + [Parameter] + public EventCallback OnUndoTriggered { get; set; } + + private int UndoCount => GameState.Value.UndoStack.Count; + private bool CanUndo => UndoCount > 0 || AlwaysShow; + private bool IsDisabled => UndoCount == 0 || GameState.Value.IsSaving; + + private string ButtonText + { + get + { + if (!string.IsNullOrEmpty(CustomText)) + return CustomText; + + var baseText = "Letzten Wurf rückgängig"; + return ShowCount && UndoCount > 0 + ? $"{baseText} ({UndoCount})" + : baseText; + } + } + + private async Task HandleUndo() + { + if (UndoCount == 0) + return; + + var lastSnapshot = GameState.Value.UndoStack[^1]; + + Dispatcher.Dispatch(new UndoThrowSuccessAction( + lastSnapshot.ThrowPanel, + lastSnapshot.Participants, + lastSnapshot.GameModel)); + + await OnUndoTriggered.InvokeAsync(); + } +} diff --git a/src/Koogle.Web/Store/GameState/GameEffects.cs b/src/Koogle.Web/Store/GameState/GameEffects.cs index 4a35682..5f9e112 100644 --- a/src/Koogle.Web/Store/GameState/GameEffects.cs +++ b/src/Koogle.Web/Store/GameState/GameEffects.cs @@ -10,24 +10,44 @@ namespace Koogle.Web.Store.GameState; public class GameEffects { private readonly ILogger _logger; + private readonly IState _gameState; /// /// Initializes a new instance of the GameEffects class. /// - public GameEffects(ILogger logger) + public GameEffects(ILogger logger, IState gameState) { _logger = logger; + _gameState = gameState; } /// - /// Handles UndoThrowAction - pops last snapshot from stack. + /// Handles UndoThrowAction - pops last snapshot from stack and restores state. /// [EffectMethod] public Task HandleUndoThrow(UndoThrowAction action, IDispatcher dispatcher) { - // Note: Actual state access will be handled via IState injection in later phases - // For now, this is a placeholder that the reducer handles directly - _logger.LogDebug("Undo throw action received"); + var state = _gameState.Value; + + if (state.UndoStack.Count == 0) + { + _logger.LogWarning("Undo requested but undo stack is empty"); + dispatcher.Dispatch(new UndoThrowFailureAction("Keine Würfe zum Rückgängigmachen vorhanden")); + return Task.CompletedTask; + } + + var lastSnapshot = state.UndoStack[^1]; + + _logger.LogDebug( + "Undoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}", + state.UndoStack.Count, + lastSnapshot.ThrowPanel.TotalThrowCounter); + + dispatcher.Dispatch(new UndoThrowSuccessAction( + lastSnapshot.ThrowPanel, + lastSnapshot.Participants, + lastSnapshot.GameModel)); + return Task.CompletedTask; }