From a56b3fc4515156276d94ada8f6e07fb39ea5d1f4 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 26 Feb 2026 18:07:01 +0100 Subject: [PATCH] Extract shared header and skill group block components --- README.md | 1 + .../Pages/HomeControls/AdminHome.razor | 141 +++++++-------- .../Pages/HomeControls/AdminHome.razor.cs | 26 ++- .../Pages/HomeControls/AppHeader.razor | 53 ++++++ .../Pages/HomeControls/AppHeader.razor.cs | 60 +++++++ .../Pages/HomeControls/CharacterPanel.razor | 170 ++++-------------- .../HomeControls/CharacterPanel.razor.cs | 21 +++ .../Pages/HomeControls/SkillGroupBlock.razor | 80 +++++++++ .../HomeControls/SkillGroupBlock.razor.cs | 60 +++++++ RpgRoller/Components/Pages/Workspace.razor | 66 ++----- RpgRoller/Components/Pages/Workspace.razor.cs | 18 +- 11 files changed, 434 insertions(+), 262 deletions(-) create mode 100644 RpgRoller/Components/Pages/HomeControls/AppHeader.razor create mode 100644 RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs create mode 100644 RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor create mode 100644 RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs diff --git a/README.md b/README.md index c429d3d..c782a76 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Gameplay capabilities now include: - Campaign management owner labels use account display names (no GUID fallback rendering) - Character edit flow supports unlinking from campaigns (owner/GM/admin) and assigning to any existing campaign via expanded campaign options - Campaign management supports character deletion by character owner or admin +- Shared top header control across workspace and admin views (consistent navigation/logout behavior) ## Prerequisites diff --git a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor index ced8447..2de9760 100644 --- a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor +++ b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor @@ -1,76 +1,71 @@
-
-
-
-

Admin

-
- @if (CurrentUser is null) - { -

Loading admin session...

- } - else - { -

@CurrentUser.DisplayName (@CurrentUser.Username)

- } +
+ -
- - Logout -
- @if (!string.IsNullOrWhiteSpace(StatusMessage)) - { -

@StatusMessage

- } -
- -
-
-

User Management

-
- @if (IsLoading) - { -

Loading users...

- } - else if (!IsCurrentUserAdmin) - { -

Admin role is required to manage users.

- } - else if (Users.Count == 0) - { -

No users found.

- } - else - { -
    - @foreach (var user in Users) - { - var userIsAdmin = HasAdminRole(user); -
  • -
    - @user.Username -

    @user.DisplayName

    -

    Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))

    -
    -
    - - -
    -
  • - } -
- } -
-
+
+
+
+

User Management

+
+ @if (!string.IsNullOrWhiteSpace(StatusMessage)) + { +

@StatusMessage

+ } + @if (IsLoading) + { +

Loading users...

+ } + else if (!IsCurrentUserAdmin) + { +

Admin role is required to manage users.

+ } + else if (Users.Count == 0) + { +

No users found.

+ } + else + { +
    + @foreach (var user in Users) + { +
  • +
    + @user.Username +

    @user.DisplayName

    +

    Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))

    +
    +
    + + +
    +
  • + } +
+ } +
+
+
diff --git a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs index 9da44c6..d7cd47f 100644 --- a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs @@ -46,11 +46,18 @@ public partial class AdminHome } } - private async Task BackToWorkspaceAsync() + private async Task OpenWorkspaceAsync() { + IsScreenMenuOpen = false; await WorkspaceRequested.InvokeAsync(); } + private Task OpenAdminAsync() + { + IsScreenMenuOpen = false; + return Task.CompletedTask; + } + private async Task LogoutAsync() { if (IsMutating) @@ -148,6 +155,11 @@ public partial class AdminHome StatusIsError = isError; } + private void ToggleScreenMenu() + { + IsScreenMenuOpen = !IsScreenMenuOpen; + } + [Inject] private RpgRollerApiClient ApiClient { get; set; } = null!; @@ -156,11 +168,23 @@ public partial class AdminHome private bool IsLoading { get; set; } = true; private bool IsMutating { get; set; } + private bool IsScreenMenuOpen { get; set; } private bool IsCurrentUserAdmin { get; set; } private UserSummary? CurrentUser { get; set; } private List Users { get; set; } = []; private string? StatusMessage { get; set; } private bool StatusIsError { get; set; } + private IReadOnlyList HeaderMenuItems + { + get + { + return + [ + new AppHeaderMenuItem { Label = "Workspace", IsActive = false, OnSelected = OpenWorkspaceAsync }, + new AppHeaderMenuItem { Label = "Admin", IsActive = true, OnSelected = OpenAdminAsync } + ]; + } + } [Parameter] public EventCallback LoggedOut { get; set; } diff --git a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor new file mode 100644 index 0000000..fda3095 --- /dev/null +++ b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor @@ -0,0 +1,53 @@ +
+
+

@Title

+ @if (User is null) + { +

Loading user...

+ } + else + { +

@User.DisplayName (@User.Username)

+ } + @if (ShowCampaign) + { +

Campaign: @(CampaignName ?? "No campaign selected")

+ } + @if (ShowConnectionState) + { +
+

@ConnectionStateLabel

+
+ } + Logout + @if (MenuItems.Count > 0) + { +
+ + @if (IsMenuOpen) + { + + } +
+ } +
+
diff --git a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs new file mode 100644 index 0000000..83dd5f7 --- /dev/null +++ b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using RpgRoller.Contracts; + +namespace RpgRoller.Components.Pages.HomeControls; + +[ExcludeFromCodeCoverage] +public partial class AppHeader +{ + private Task SelectMenuItemAsync(AppHeaderMenuItem item) + { + return item.OnSelected?.Invoke() ?? Task.CompletedTask; + } + + [Parameter] + public string Title { get; set; } = "RpgRoller"; + + [Parameter] + public UserSummary? User { get; set; } + + [Parameter] + public bool ShowCampaign { get; set; } + + [Parameter] + public string? CampaignName { get; set; } + + [Parameter] + public bool ShowConnectionState { get; set; } = true; + + [Parameter] + public string ConnectionStateLabel { get; set; } = "Offline fallback"; + + [Parameter] + public string ConnectionStateCssClass { get; set; } = "offline"; + + [Parameter] + public bool IsMenuOpen { get; set; } + + [Parameter] + public string MenuButtonId { get; set; } = "screen-menu-button"; + + [Parameter] + public string MenuId { get; set; } = "screen-menu"; + + [Parameter] + public IReadOnlyList MenuItems { get; set; } = []; + + [Parameter] + public EventCallback ToggleMenuRequested { get; set; } + + [Parameter] + public EventCallback LogoutRequested { get; set; } +} + +public sealed class AppHeaderMenuItem +{ + public string Label { get; init; } = string.Empty; + public bool IsActive { get; init; } + public Func? OnSelected { get; init; } +} diff --git a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor index d2fb36e..517cfbb 100644 --- a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor +++ b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor @@ -79,143 +79,45 @@ @foreach (var group in visibleSkillGroups) { var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList(); -
-
- @group.Name -
- - -
-
- @if (!hasSkillFilter && groupSkills.Count == 0) - { -

No skills in this group yet.

- } -
- @foreach (var skill in groupSkills) - { -
-
- @skill.Name - @SkillDefinitionLabel(skill) -
-
- - - -
-
- } - -
-
+ } @if (!hasSkillFilter || ungroupedSkills.Count > 0) { -
-
- Ungrouped -
- @if (!hasSkillFilter && ungroupedSkills.Count == 0) - { -

No ungrouped skills.

- } -
- @foreach (var skill in ungroupedSkills) - { -
-
- @skill.Name - @SkillDefinitionLabel(skill) -
-
- - - -
-
- } - -
-
+ } + + + } + + @if (!HasSkillFilter && Skills.Count == 0) + { +

@EmptyMessage

+ } +
+ @foreach (var skill in Skills) + { +
+
+ @skill.Name + @SkillDefinitionLabel(skill) +
+
+ + + +
+
+ } + +
+ diff --git a/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs new file mode 100644 index 0000000..0e38a93 --- /dev/null +++ b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using RpgRoller.Contracts; + +namespace RpgRoller.Components.Pages.HomeControls; + +[ExcludeFromCodeCoverage] +public partial class SkillGroupBlock +{ + [Parameter] + public string Title { get; set; } = string.Empty; + + [Parameter] + public Guid? SkillGroupId { get; set; } + + [Parameter] + public IReadOnlyList Skills { get; set; } = []; + + [Parameter] + public bool IsMutating { get; set; } + + [Parameter] + public bool CanEditGroup { get; set; } + + [Parameter] + public bool CanCreateSkill { get; set; } + + [Parameter] + public bool HasSkillFilter { get; set; } + + [Parameter] + public string EmptyMessage { get; set; } = "No skills in this group yet."; + + [Parameter] + public bool ShowGroupActions { get; set; } + + [Parameter] + public Func CanEditSkill { get; set; } = _ => false; + + [Parameter] + public Func SkillDefinitionLabel { get; set; } = _ => string.Empty; + + [Parameter] + public EventCallback AddSkillRequested { get; set; } + + [Parameter] + public EventCallback EditSkillRequested { get; set; } + + [Parameter] + public EventCallback RollSkillRequested { get; set; } + + [Parameter] + public EventCallback DeleteSkillRequested { get; set; } + + [Parameter] + public EventCallback EditGroupRequested { get; set; } + + [Parameter] + public EventCallback DeleteGroupRequested { get; set; } +} diff --git a/RpgRoller/Components/Pages/Workspace.razor b/RpgRoller/Components/Pages/Workspace.razor index 7bb53a5..7f61ff7 100644 --- a/RpgRoller/Components/Pages/Workspace.razor +++ b/RpgRoller/Components/Pages/Workspace.razor @@ -14,59 +14,19 @@ }
-
-
-

RpgRoller

- @if (User is null) - { -

Loading user...

- } - else - { -

@User.DisplayName (@User.Username)

- } -

Campaign: @(SelectedCampaignName ?? "No campaign selected")

-
-

@ConnectionStateLabel

-
- Logout -
- - @if (IsScreenMenuOpen) - { - - } -
-
-
+ @if (IsPlayScreen) { diff --git a/RpgRoller/Components/Pages/Workspace.razor.cs b/RpgRoller/Components/Pages/Workspace.razor.cs index 1375f07..f05fec4 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using RpgRoller.Components.Pages.HomeControls; using RpgRoller.Contracts; using RpgRoller.Domain; @@ -846,7 +847,22 @@ public partial class Workspace : IAsyncDisposable private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase); private bool IsManagementScreen => !IsPlayScreen; - private string CurrentScreenLabel => IsPlayScreen ? "Play" : "Campaign Management"; + private IReadOnlyList HeaderMenuItems + { + get + { + var items = new List + { + new() { Label = "Play", IsActive = IsPlayScreen, OnSelected = SwitchToPlayAsync }, + new() { Label = "Campaign Management", IsActive = IsManagementScreen, OnSelected = SwitchToManagementAsync } + }; + + if (IsCurrentUserAdmin) + items.Add(new AppHeaderMenuItem { Label = "Admin", IsActive = false, OnSelected = OpenAdminAsync }); + + return items; + } + } private string ConnectionStateLabel => ConnectionState switch {