From c33a6f9d914cf5133320efaea552f111b8fd1fb0 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 27 Dec 2025 14:18:45 +0100 Subject: [PATCH] add ThrowTimer --- .../Components/Game/ThrowTimer.razor | 40 ++++++++++++++ .../Components/Pages/Days/DayDetails.razor | 45 +++++++++++++++- .../Store/TimerState/TimerActions.cs | 21 ++++++++ .../Store/TimerState/TimerEffects.cs | 54 +++++++++++++++++++ .../Store/TimerState/TimerReducers.cs | 49 +++++++++++++++++ src/Koogle.Web/Store/TimerState/TimerState.cs | 14 +++++ 6 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/Koogle.Web/Components/Game/ThrowTimer.razor create mode 100644 src/Koogle.Web/Store/TimerState/TimerActions.cs create mode 100644 src/Koogle.Web/Store/TimerState/TimerEffects.cs create mode 100644 src/Koogle.Web/Store/TimerState/TimerReducers.cs create mode 100644 src/Koogle.Web/Store/TimerState/TimerState.cs diff --git a/src/Koogle.Web/Components/Game/ThrowTimer.razor b/src/Koogle.Web/Components/Game/ThrowTimer.razor new file mode 100644 index 0000000..c891b56 --- /dev/null +++ b/src/Koogle.Web/Components/Game/ThrowTimer.razor @@ -0,0 +1,40 @@ +@using Koogle.Web.Store.TimerState + +@inherits Fluxor.Blazor.Web.Components.FluxorComponent + +@inject IState TimerState +@inject IDispatcher Dispatcher + +@if (TimerState.Value.IsRunning) +{ + + @TimerState.Value.RemainingSeconds + +} + + + +@code { + /// + /// Callback when timer is aborted by user click. + /// + [Parameter] + public EventCallback OnTimerAborted { get; set; } + + private async Task AbortTimer() + { + Dispatcher.Dispatch(new StopTimerAction()); + await OnTimerAborted.InvokeAsync(); + } +} diff --git a/src/Koogle.Web/Components/Pages/Days/DayDetails.razor b/src/Koogle.Web/Components/Pages/Days/DayDetails.razor index b9229fb..6bf2b44 100644 --- a/src/Koogle.Web/Components/Pages/Days/DayDetails.razor +++ b/src/Koogle.Web/Components/Pages/Days/DayDetails.razor @@ -2,6 +2,7 @@ @attribute [Authorize(Policy = "ClubViewer")] @inherits Fluxor.Blazor.Web.Components.FluxorComponent +@implements IDisposable @using Fluxor @using Koogle.Application.DTOs @@ -9,12 +10,14 @@ @using Koogle.Web.Store.DayState @using Koogle.Web.Store.GameState @using Koogle.Web.Store.PersonState +@using Koogle.Web.Store.TimerState @using Koogle.Web.Components.Game @using Microsoft.AspNetCore.Authorization @inject IState DayState @inject IState GameState @inject IState PersonState +@inject IState TimerState @inject IDispatcher Dispatcher @inject ISnackbar Snackbar @inject IDialogService DialogService @@ -426,6 +429,12 @@ else + @if (TimerState.Value.IsRunning) + { +
+ +
+ }
@@ -835,9 +844,43 @@ else Snackbar.Add("Spieler-Auswahl noch nicht implementiert", Severity.Info); } + private const int TimerDurationSeconds = 3; + private Task HandleThrowCompleted(ThrowResult result) { - // Throw was completed - game logic will handle state updates + // Switch to Tafel tab and start timer + _activeTabIndex = 2; + Dispatcher.Dispatch(new StartTimerAction(TimerDurationSeconds)); return Task.CompletedTask; } + + private void HandleTimerAborted() + { + // User clicked timer button - switch back to Eingabe tab + _activeTabIndex = 1; + } + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + TimerState.StateChanged += OnTimerStateChanged; + } + } + + private void OnTimerStateChanged(object? sender, EventArgs e) + { + // When timer stops (completed naturally), switch back to Eingabe tab + if (!TimerState.Value.IsRunning && _activeTabIndex == 2) + { + _activeTabIndex = 1; + InvokeAsync(StateHasChanged); + } + } + + public void Dispose() + { + TimerState.StateChanged -= OnTimerStateChanged; + } } diff --git a/src/Koogle.Web/Store/TimerState/TimerActions.cs b/src/Koogle.Web/Store/TimerState/TimerActions.cs new file mode 100644 index 0000000..bab864c --- /dev/null +++ b/src/Koogle.Web/Store/TimerState/TimerActions.cs @@ -0,0 +1,21 @@ +namespace Koogle.Web.Store.TimerState; + +/// +/// Action to start the throw timer. +/// +public record StartTimerAction(int Seconds); + +/// +/// Action dispatched when timer completes naturally. +/// +public record TimerCompletedAction; + +/// +/// Action to stop the timer manually (user click). +/// +public record StopTimerAction; + +/// +/// Action dispatched every second while timer is running. +/// +public record TickAction; diff --git a/src/Koogle.Web/Store/TimerState/TimerEffects.cs b/src/Koogle.Web/Store/TimerState/TimerEffects.cs new file mode 100644 index 0000000..479f7f7 --- /dev/null +++ b/src/Koogle.Web/Store/TimerState/TimerEffects.cs @@ -0,0 +1,54 @@ +using Fluxor; + +namespace Koogle.Web.Store.TimerState; + +/// +/// Effects for handling timer async operations. +/// +public class TimerEffects(ILogger logger) +{ + private CancellationTokenSource? _timerCancellation; + + [EffectMethod] + public async Task HandleStartTimer(StartTimerAction action, IDispatcher dispatcher) + { + logger.LogInformation("Timer started for {Seconds} seconds", action.Seconds); + + // Cancel previous timer if running + if (_timerCancellation != null) + { + await _timerCancellation.CancelAsync(); + } + + _timerCancellation = new CancellationTokenSource(); + + try + { + for (var i = 0; i < action.Seconds; i++) + { + var token = _timerCancellation?.Token ?? CancellationToken.None; + await Task.Delay(1000, token); + dispatcher.Dispatch(new TickAction()); + } + + // Timer completed naturally + dispatcher.Dispatch(new TimerCompletedAction()); + logger.LogInformation("Timer completed"); + } + catch (TaskCanceledException) + { + logger.LogInformation("Timer was cancelled"); + } + } + + [EffectMethod] + public async Task HandleStopTimer(StopTimerAction action, IDispatcher dispatcher) + { + if (_timerCancellation is { IsCancellationRequested: false }) + { + logger.LogInformation("Timer stopped manually"); + await _timerCancellation.CancelAsync(); + _timerCancellation = null; + } + } +} diff --git a/src/Koogle.Web/Store/TimerState/TimerReducers.cs b/src/Koogle.Web/Store/TimerState/TimerReducers.cs new file mode 100644 index 0000000..3866fbe --- /dev/null +++ b/src/Koogle.Web/Store/TimerState/TimerReducers.cs @@ -0,0 +1,49 @@ +using Fluxor; + +namespace Koogle.Web.Store.TimerState; + +public static class TimerReducers +{ + [ReducerMethod] + public static TimerState OnStartTimer(TimerState state, StartTimerAction action) + { + return state with + { + RemainingSeconds = action.Seconds, + IsRunning = true + }; + } + + [ReducerMethod] + public static TimerState OnTick(TimerState state, TickAction action) + { + if (!state.IsRunning || state.RemainingSeconds <= 0) + return state; + + return state with + { + RemainingSeconds = state.RemainingSeconds - 1, + IsRunning = state.RemainingSeconds - 1 > 0 + }; + } + + [ReducerMethod] + public static TimerState OnTimerCompleted(TimerState state, TimerCompletedAction action) + { + return state with + { + IsRunning = false, + RemainingSeconds = 0 + }; + } + + [ReducerMethod] + public static TimerState OnStopTimer(TimerState state, StopTimerAction action) + { + return state with + { + IsRunning = false, + RemainingSeconds = 0 + }; + } +} diff --git a/src/Koogle.Web/Store/TimerState/TimerState.cs b/src/Koogle.Web/Store/TimerState/TimerState.cs new file mode 100644 index 0000000..c7157e6 --- /dev/null +++ b/src/Koogle.Web/Store/TimerState/TimerState.cs @@ -0,0 +1,14 @@ +using Fluxor; + +namespace Koogle.Web.Store.TimerState; + +/// +/// Fluxor state for throw timer management. +/// +[FeatureState] +public record TimerState(int RemainingSeconds, bool IsRunning) +{ + public TimerState() : this(0, false) + { + } +}