Complete phase H8: Undo functionality (unbegrenzt)

- 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 <noreply@anthropic.com>
This commit is contained in:
beo3000 2025-12-27 09:11:40 +01:00
parent aefa676d62
commit e436ee2754
4 changed files with 132 additions and 30 deletions

View File

@ -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 |

View File

@ -51,18 +51,9 @@
</div>
@* Undo button *@
@if (GameState.Value.UndoStack.Count > 0)
{
<div class="undo-section">
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
FullWidth="true"
OnClick="HandleUndo"
StartIcon="@Icons.Material.Filled.Undo">
Letzten Wurf rückgängig (@GameState.Value.UndoStack.Count)
</MudButton>
<UndoButton />
</div>
}
@* 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();

View File

@ -0,0 +1,103 @@
@using Koogle.Web.Store.GameState
@inherits FluxorComponent
@inject IState<GameState> GameState
@inject IDispatcher Dispatcher
@* Only show when undo is available *@
@if (CanUndo)
{
<MudButton Variant="@Variant"
Color="Color.Secondary"
FullWidth="@FullWidth"
Size="@Size"
Disabled="@IsDisabled"
OnClick="HandleUndo"
StartIcon="@Icons.Material.Filled.Undo"
Class="@Class">
@ButtonText
</MudButton>
}
@code {
/// <summary>
/// Whether to show the button even when no undo is available.
/// </summary>
[Parameter]
public bool AlwaysShow { get; set; }
/// <summary>
/// Button variant (default: Outlined).
/// </summary>
[Parameter]
public Variant Variant { get; set; } = Variant.Outlined;
/// <summary>
/// Whether button should be full width.
/// </summary>
[Parameter]
public bool FullWidth { get; set; } = true;
/// <summary>
/// Button size.
/// </summary>
[Parameter]
public Size Size { get; set; } = Size.Medium;
/// <summary>
/// Additional CSS classes.
/// </summary>
[Parameter]
public string? Class { get; set; }
/// <summary>
/// Optional custom button text. If not set, shows count.
/// </summary>
[Parameter]
public string? CustomText { get; set; }
/// <summary>
/// Whether to show the undo count in the button text.
/// </summary>
[Parameter]
public bool ShowCount { get; set; } = true;
/// <summary>
/// Callback when undo is triggered.
/// </summary>
[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();
}
}

View File

@ -10,24 +10,44 @@ namespace Koogle.Web.Store.GameState;
public class GameEffects
{
private readonly ILogger<GameEffects> _logger;
private readonly IState<GameState> _gameState;
/// <summary>
/// Initializes a new instance of the GameEffects class.
/// </summary>
public GameEffects(ILogger<GameEffects> logger)
public GameEffects(ILogger<GameEffects> logger, IState<GameState> gameState)
{
_logger = logger;
_gameState = gameState;
}
/// <summary>
/// Handles UndoThrowAction - pops last snapshot from stack.
/// Handles UndoThrowAction - pops last snapshot from stack and restores state.
/// </summary>
[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;
}