Extract shared header and skill group block components

This commit is contained in:
2026-02-26 18:07:01 +01:00
parent 51d04fcdc5
commit a56b3fc451
11 changed files with 434 additions and 262 deletions

View File

@@ -56,6 +56,7 @@ Gameplay capabilities now include:
- Campaign management owner labels use account display names (no GUID fallback rendering) - 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 - 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 - Campaign management supports character deletion by character owner or admin
- Shared top header control across workspace and admin views (consistent navigation/logout behavior)
## Prerequisites ## Prerequisites

View File

@@ -1,76 +1,71 @@
<div class="rr-app"> <div class="rr-app">
<main class="management-screen"> <div class="workspace-shell">
<section class="card"> <AppHeader
<div class="section-head"> User="CurrentUser"
<h2>Admin</h2> ShowCampaign="false"
</div> ShowConnectionState="true"
@if (CurrentUser is null) ConnectionStateLabel="@(!IsLoading && CurrentUser is not null ? "Connected" : "Offline fallback")"
{ ConnectionStateCssClass="@(!IsLoading && CurrentUser is not null ? "ok" : "offline")"
<p class="empty">Loading admin session...</p> IsMenuOpen="IsScreenMenuOpen"
} MenuButtonId="admin-screen-menu-button"
else MenuId="admin-screen-menu"
{ MenuItems="HeaderMenuItems"
<p class="muted"><strong>@CurrentUser.DisplayName</strong> (@CurrentUser.Username)</p> ToggleMenuRequested="ToggleScreenMenu"
} LogoutRequested="LogoutAsync"/>
<div class="inline-actions"> <main class="management-screen">
<button type="button" class="ghost" disabled="@(IsMutating || IsLoading)" @onclick="BackToWorkspaceAsync">Back to workspace</button> <section class="card">
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a> <div class="section-head">
</div> <h2>User Management</h2>
@if (!string.IsNullOrWhiteSpace(StatusMessage)) </div>
{ @if (!string.IsNullOrWhiteSpace(StatusMessage))
<p class="@(StatusIsError ? "form-error" : "muted")">@StatusMessage</p> {
} <p class="@(StatusIsError ? "form-error" : "muted")">@StatusMessage</p>
</section> }
@if (IsLoading)
<section class="card"> {
<div class="section-head"> <p class="empty">Loading users...</p>
<h2>User Management</h2> }
</div> else if (!IsCurrentUserAdmin)
@if (IsLoading) {
{ <p class="empty">Admin role is required to manage users.</p>
<p class="empty">Loading users...</p> }
} else if (Users.Count == 0)
else if (!IsCurrentUserAdmin) {
{ <p class="empty">No users found.</p>
<p class="empty">Admin role is required to manage users.</p> }
} else
else if (Users.Count == 0) {
{ <ul class="management-list">
<p class="empty">No users found.</p> @foreach (var user in Users)
} {
else <li>
{ <div>
<ul class="management-list"> <strong>@user.Username</strong>
@foreach (var user in Users) <p class="muted">@user.DisplayName</p>
{ <p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
var userIsAdmin = HasAdminRole(user); </div>
<li> <div class="skill-chip-actions">
<div> <button type="button"
<strong>@user.Username</strong> class="chip-button"
<p class="muted">@user.DisplayName</p> disabled="@(IsMutating || user.Id == CurrentUser?.Id)"
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p> @onclick="() => ToggleAdminRoleAsync(user)">
</div> <span aria-hidden="true" class="emoji">🛡️</span>
<div class="skill-chip-actions"> <span class="sr-only">Toggle admin role for @user.Username</span>
<button type="button" </button>
class="chip-button" <button type="button"
disabled="@(IsMutating || user.Id == CurrentUser?.Id)" class="chip-button"
@onclick="() => ToggleAdminRoleAsync(user)"> disabled="@(IsMutating || user.Id == CurrentUser?.Id)"
<span aria-hidden="true" class="emoji">🛡️</span> @onclick="() => DeleteUserAsync(user)">
<span class="sr-only">Toggle admin role for @user.Username</span> <span aria-hidden="true" class="emoji">🗑️</span>
</button> <span class="sr-only">Delete user @user.Username</span>
<button type="button" </button>
class="chip-button" </div>
disabled="@(IsMutating || user.Id == CurrentUser?.Id)" </li>
@onclick="() => DeleteUserAsync(user)"> }
<span aria-hidden="true" class="emoji">🗑️</span> </ul>
<span class="sr-only">Delete user @user.Username</span> }
</button> </section>
</div> </main>
</li> </div>
}
</ul>
}
</section>
</main>
</div> </div>

View File

@@ -46,11 +46,18 @@ public partial class AdminHome
} }
} }
private async Task BackToWorkspaceAsync() private async Task OpenWorkspaceAsync()
{ {
IsScreenMenuOpen = false;
await WorkspaceRequested.InvokeAsync(); await WorkspaceRequested.InvokeAsync();
} }
private Task OpenAdminAsync()
{
IsScreenMenuOpen = false;
return Task.CompletedTask;
}
private async Task LogoutAsync() private async Task LogoutAsync()
{ {
if (IsMutating) if (IsMutating)
@@ -148,6 +155,11 @@ public partial class AdminHome
StatusIsError = isError; StatusIsError = isError;
} }
private void ToggleScreenMenu()
{
IsScreenMenuOpen = !IsScreenMenuOpen;
}
[Inject] [Inject]
private RpgRollerApiClient ApiClient { get; set; } = null!; private RpgRollerApiClient ApiClient { get; set; } = null!;
@@ -156,11 +168,23 @@ public partial class AdminHome
private bool IsLoading { get; set; } = true; private bool IsLoading { get; set; } = true;
private bool IsMutating { get; set; } private bool IsMutating { get; set; }
private bool IsScreenMenuOpen { get; set; }
private bool IsCurrentUserAdmin { get; set; } private bool IsCurrentUserAdmin { get; set; }
private UserSummary? CurrentUser { get; set; } private UserSummary? CurrentUser { get; set; }
private List<AdminUserSummary> Users { get; set; } = []; private List<AdminUserSummary> Users { get; set; } = [];
private string? StatusMessage { get; set; } private string? StatusMessage { get; set; }
private bool StatusIsError { get; set; } private bool StatusIsError { get; set; }
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
{
get
{
return
[
new AppHeaderMenuItem { Label = "Workspace", IsActive = false, OnSelected = OpenWorkspaceAsync },
new AppHeaderMenuItem { Label = "Admin", IsActive = true, OnSelected = OpenAdminAsync }
];
}
}
[Parameter] [Parameter]
public EventCallback<string?> LoggedOut { get; set; } public EventCallback<string?> LoggedOut { get; set; }

View File

@@ -0,0 +1,53 @@
<header class="workspace-header">
<div class="header-row">
<h1>@Title</h1>
@if (User is null)
{
<p class="header-identity"><strong>Loading user...</strong></p>
}
else
{
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
}
@if (ShowCampaign)
{
<p class="header-campaign">Campaign: <strong>@(CampaignName ?? "No campaign selected")</strong></p>
}
@if (ShowConnectionState)
{
<div class="header-connection-cell">
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
</div>
}
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutRequested">Logout</a>
@if (MenuItems.Count > 0)
{
<div class="header-menu-wrap">
<button
id="@MenuButtonId"
type="button"
class="menu-toggle"
aria-haspopup="true"
aria-expanded="@IsMenuOpen"
aria-controls="@MenuId"
@onclick="ToggleMenuRequested">
<span aria-hidden="true">☰</span>
</button>
@if (IsMenuOpen)
{
<div id="@MenuId" class="screen-menu" role="menu" aria-labelledby="@MenuButtonId">
@foreach (var item in MenuItems)
{
<button type="button"
class="menu-item @(item.IsActive ? "active" : string.Empty)"
role="menuitem"
@onclick="() => SelectMenuItemAsync(item)">
@item.Label
</button>
}
</div>
}
</div>
}
</div>
</header>

View File

@@ -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<AppHeaderMenuItem> 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<Task>? OnSelected { get; init; }
}

View File

@@ -79,143 +79,45 @@
@foreach (var group in visibleSkillGroups) @foreach (var group in visibleSkillGroups)
{ {
var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList(); var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
<div class="skill-group-block"> <SkillGroupBlock
<div class="skill-group-head"> Title="@group.Name"
<strong>@group.Name</strong> SkillGroupId="group.Id"
<div class="skill-chip-actions"> Skills="groupSkills"
<button IsMutating="IsMutating"
type="button" CanEditGroup="CanEditCharacter(SelectedCharacter)"
class="chip-button" CanCreateSkill="CanEditCharacter(SelectedCharacter)"
title="Edit skill group" HasSkillFilter="hasSkillFilter"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))" EmptyMessage="No skills in this group yet."
@onclick="() => OpenEditSkillGroupModal(group)"> ShowGroupActions="true"
<span aria-hidden="true" class="emoji">✏️</span> CanEditSkill="CanEditSkill"
<span class="sr-only">Edit @group.Name</span> SkillDefinitionLabel="SkillDefinitionLabel"
</button> AddSkillRequested="OnAddSkillRequestedAsync"
<button EditSkillRequested="OnEditSkillRequestedAsync"
type="button" RollSkillRequested="RollSkillAsync"
class="chip-button" DeleteSkillRequested="DeleteSkillAsync"
title="Delete skill group" EditGroupRequested="OnEditSkillGroupRequestedAsync"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))" DeleteGroupRequested="DeleteSkillGroupAsync"/>
@onclick="() => DeleteSkillGroupAsync(group.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @group.Name</span>
</button>
</div>
</div>
@if (!hasSkillFilter && groupSkills.Count == 0)
{
<p class="empty">No skills in this group yet.</p>
}
<div class="skill-list">
@foreach (var skill in groupSkills)
{
<div class="skill-item">
<div class="skill-details">
<strong>@skill.Name</strong>
<span>@SkillDefinitionLabel(skill)</span>
</div>
<div class="skill-chip-actions">
<button
type="button"
class="chip-button"
title="Edit skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => OpenEditSkillModal(skill)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Roll skill"
disabled="@(IsMutating)"
@onclick="() => RollSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🎲</span>
<span class="sr-only">Roll @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => DeleteSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @skill.Name</span>
</button>
</div>
</div>
}
<button
type="button"
class="skill-item create-skill-item"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
@onclick="() => OpenCreateSkillModal(group.Id)">
<span class="skill-create-icon" aria-hidden="true">+</span>
<span>Add skill</span>
</button>
</div>
</div>
} }
@if (!hasSkillFilter || ungroupedSkills.Count > 0) @if (!hasSkillFilter || ungroupedSkills.Count > 0)
{ {
<div class="skill-group-block"> <SkillGroupBlock
<div class="skill-group-head"> Title="Ungrouped"
<strong>Ungrouped</strong> SkillGroupId="@((Guid?)null)"
</div> Skills="ungroupedSkills"
@if (!hasSkillFilter && ungroupedSkills.Count == 0) IsMutating="IsMutating"
{ CanEditGroup="false"
<p class="empty">No ungrouped skills.</p> CanCreateSkill="CanEditCharacter(SelectedCharacter)"
} HasSkillFilter="hasSkillFilter"
<div class="skill-list"> EmptyMessage="No ungrouped skills."
@foreach (var skill in ungroupedSkills) ShowGroupActions="false"
{ CanEditSkill="CanEditSkill"
<div class="skill-item"> SkillDefinitionLabel="SkillDefinitionLabel"
<div class="skill-details"> AddSkillRequested="OnAddSkillRequestedAsync"
<strong>@skill.Name</strong> EditSkillRequested="OnEditSkillRequestedAsync"
<span>@SkillDefinitionLabel(skill)</span> RollSkillRequested="RollSkillAsync"
</div> DeleteSkillRequested="DeleteSkillAsync"
<div class="skill-chip-actions"> EditGroupRequested="OnEditSkillGroupRequestedAsync"
<button DeleteGroupRequested="DeleteSkillGroupAsync"/>
type="button"
class="chip-button"
title="Edit skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => OpenEditSkillModal(skill)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Roll skill"
disabled="@(IsMutating)"
@onclick="() => RollSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🎲</span>
<span class="sr-only">Roll @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => DeleteSkillAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @skill.Name</span>
</button>
</div>
</div>
}
<button
type="button"
class="skill-item create-skill-item"
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
@onclick="() => OpenCreateSkillModal(null)">
<span class="skill-create-icon" aria-hidden="true">+</span>
<span>Add skill</span>
</button>
</div>
</div>
} }
<button <button

View File

@@ -72,6 +72,27 @@ public partial class CharacterPanel
await RollRequested.InvokeAsync(skillId); await RollRequested.InvokeAsync(skillId);
} }
private Task OnAddSkillRequestedAsync(Guid? skillGroupId)
{
OpenCreateSkillModal(skillGroupId);
return Task.CompletedTask;
}
private Task OnEditSkillRequestedAsync(SkillSummary skill)
{
OpenEditSkillModal(skill);
return Task.CompletedTask;
}
private Task OnEditSkillGroupRequestedAsync(Guid skillGroupId)
{
var skillGroup = SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId);
if (skillGroup is not null)
OpenEditSkillGroupModal(skillGroup);
return Task.CompletedTask;
}
private void OpenCreateSkillGroupModal() private void OpenCreateSkillGroupModal()
{ {
SkillGroupState.Model.Name = string.Empty; SkillGroupState.Model.Name = string.Empty;

View File

@@ -0,0 +1,80 @@
<div class="skill-group-block">
<div class="skill-group-head">
<strong>@Title</strong>
@if (ShowGroupActions && SkillGroupId.HasValue)
{
<div class="skill-chip-actions">
<button
type="button"
class="chip-button"
title="Edit skill group"
disabled="@(IsMutating || !CanEditGroup)"
@onclick="() => EditGroupRequested.InvokeAsync(SkillGroupId.Value)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @Title</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill group"
disabled="@(IsMutating || !CanEditGroup)"
@onclick="() => DeleteGroupRequested.InvokeAsync(SkillGroupId.Value)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @Title</span>
</button>
</div>
}
</div>
@if (!HasSkillFilter && Skills.Count == 0)
{
<p class="empty">@EmptyMessage</p>
}
<div class="skill-list">
@foreach (var skill in Skills)
{
<div class="skill-item">
<div class="skill-details">
<strong>@skill.Name</strong>
<span>@SkillDefinitionLabel(skill)</span>
</div>
<div class="skill-chip-actions">
<button
type="button"
class="chip-button"
title="Edit skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => EditSkillRequested.InvokeAsync(skill)">
<span aria-hidden="true" class="emoji">✏️</span>
<span class="sr-only">Edit @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Roll skill"
disabled="@(IsMutating)"
@onclick="() => RollSkillRequested.InvokeAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🎲</span>
<span class="sr-only">Roll @skill.Name</span>
</button>
<button
type="button"
class="chip-button"
title="Delete skill"
disabled="@(IsMutating || !CanEditSkill(skill))"
@onclick="() => DeleteSkillRequested.InvokeAsync(skill.Id)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete @skill.Name</span>
</button>
</div>
</div>
}
<button
type="button"
class="skill-item create-skill-item"
disabled="@(IsMutating || !CanCreateSkill)"
@onclick="() => AddSkillRequested.InvokeAsync(SkillGroupId)">
<span class="skill-create-icon" aria-hidden="true">+</span>
<span>Add skill</span>
</button>
</div>
</div>

View File

@@ -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<SkillSummary> 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<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
[Parameter]
public Func<SkillSummary, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
[Parameter]
public EventCallback<Guid?> AddSkillRequested { get; set; }
[Parameter]
public EventCallback<SkillSummary> EditSkillRequested { get; set; }
[Parameter]
public EventCallback<Guid> RollSkillRequested { get; set; }
[Parameter]
public EventCallback<Guid> DeleteSkillRequested { get; set; }
[Parameter]
public EventCallback<Guid> EditGroupRequested { get; set; }
[Parameter]
public EventCallback<Guid> DeleteGroupRequested { get; set; }
}

View File

@@ -14,59 +14,19 @@
} }
<div class="workspace-shell"> <div class="workspace-shell">
<header class="workspace-header"> <AppHeader
<div class="header-row"> User="User"
<h1>RpgRoller</h1> ShowCampaign="true"
@if (User is null) CampaignName="SelectedCampaignName"
{ ShowConnectionState="true"
<p class="header-identity"><strong>Loading user...</strong></p> ConnectionStateLabel="ConnectionStateLabel"
} ConnectionStateCssClass="ConnectionStateCssClass"
else IsMenuOpen="IsScreenMenuOpen"
{ MenuButtonId="workspace-screen-menu-button"
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p> MenuId="workspace-screen-menu"
} MenuItems="HeaderMenuItems"
<p class="header-campaign">Campaign: <strong>@(SelectedCampaignName ?? "No campaign selected")</strong></p> ToggleMenuRequested="ToggleScreenMenu"
<div class="header-connection-cell"> LogoutRequested="LogoutAsync"/>
<p class="connection @ConnectionStateCssClass">@ConnectionStateLabel</p>
</div>
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a>
<div class="header-menu-wrap">
<button
id="screen-menu-button"
type="button"
class="menu-toggle"
aria-haspopup="true"
aria-expanded="@IsScreenMenuOpen"
aria-controls="screen-menu"
@onclick="ToggleScreenMenu">
<span aria-hidden="true">☰</span>
</button>
@if (IsScreenMenuOpen)
{
<div id="screen-menu" class="screen-menu" role="menu" aria-labelledby="screen-menu-button">
<button type="button"
class="menu-item @(IsPlayScreen ? "active" : string.Empty)"
role="menuitem"
@onclick="SwitchToPlayAsync">Play
</button>
<button type="button"
class="menu-item @(IsManagementScreen ? "active" : string.Empty)"
role="menuitem"
@onclick="SwitchToManagementAsync">Campaign Management
</button>
@if (IsCurrentUserAdmin)
{
<button type="button"
class="menu-item"
role="menuitem"
@onclick="OpenAdminAsync">Admin
</button>
}
</div>
}
</div>
</div>
</header>
@if (IsPlayScreen) @if (IsPlayScreen)
{ {

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using RpgRoller.Components.Pages.HomeControls;
using RpgRoller.Contracts; using RpgRoller.Contracts;
using RpgRoller.Domain; using RpgRoller.Domain;
@@ -846,7 +847,22 @@ public partial class Workspace : IAsyncDisposable
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase); private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
private bool IsManagementScreen => !IsPlayScreen; private bool IsManagementScreen => !IsPlayScreen;
private string CurrentScreenLabel => IsPlayScreen ? "Play" : "Campaign Management"; private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
{
get
{
var items = new List<AppHeaderMenuItem>
{
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 private string ConnectionStateLabel => ConnectionState switch
{ {