Extract shared header and skill group block components
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
<div class="rr-app">
|
||||
<div class="workspace-shell">
|
||||
<AppHeader
|
||||
User="CurrentUser"
|
||||
ShowCampaign="false"
|
||||
ShowConnectionState="true"
|
||||
ConnectionStateLabel="@(!IsLoading && CurrentUser is not null ? "Connected" : "Offline fallback")"
|
||||
ConnectionStateCssClass="@(!IsLoading && CurrentUser is not null ? "ok" : "offline")"
|
||||
IsMenuOpen="IsScreenMenuOpen"
|
||||
MenuButtonId="admin-screen-menu-button"
|
||||
MenuId="admin-screen-menu"
|
||||
MenuItems="HeaderMenuItems"
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="LogoutAsync"/>
|
||||
|
||||
<main class="management-screen">
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Admin</h2>
|
||||
</div>
|
||||
@if (CurrentUser is null)
|
||||
{
|
||||
<p class="empty">Loading admin session...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted"><strong>@CurrentUser.DisplayName</strong> (@CurrentUser.Username)</p>
|
||||
}
|
||||
|
||||
<div class="inline-actions">
|
||||
<button type="button" class="ghost" disabled="@(IsMutating || IsLoading)" @onclick="BackToWorkspaceAsync">Back to workspace</button>
|
||||
<a href="" class="logout-link" @onclick:preventDefault="true" @onclick="LogoutAsync">Logout</a>
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(StatusMessage))
|
||||
{
|
||||
<p class="@(StatusIsError ? "form-error" : "muted")">@StatusMessage</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p class="empty">Loading users...</p>
|
||||
@@ -44,7 +39,6 @@
|
||||
<ul class="management-list">
|
||||
@foreach (var user in Users)
|
||||
{
|
||||
var userIsAdmin = HasAdminRole(user);
|
||||
<li>
|
||||
<div>
|
||||
<strong>@user.Username</strong>
|
||||
@@ -74,3 +68,4 @@
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<AdminUserSummary> Users { get; set; } = [];
|
||||
private string? StatusMessage { 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]
|
||||
public EventCallback<string?> LoggedOut { get; set; }
|
||||
|
||||
53
RpgRoller/Components/Pages/HomeControls/AppHeader.razor
Normal file
53
RpgRoller/Components/Pages/HomeControls/AppHeader.razor
Normal 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>
|
||||
60
RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs
Normal file
60
RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs
Normal 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; }
|
||||
}
|
||||
@@ -79,143 +79,45 @@
|
||||
@foreach (var group in visibleSkillGroups)
|
||||
{
|
||||
var groupSkills = filteredSkills.Where(skill => skill.SkillGroupId == group.Id).ToList();
|
||||
<div class="skill-group-block">
|
||||
<div class="skill-group-head">
|
||||
<strong>@group.Name</strong>
|
||||
<div class="skill-chip-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Edit skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => OpenEditSkillGroupModal(group)">
|
||||
<span aria-hidden="true" class="emoji">✏️</span>
|
||||
<span class="sr-only">Edit @group.Name</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Delete skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@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>
|
||||
<SkillGroupBlock
|
||||
Title="@group.Name"
|
||||
SkillGroupId="group.Id"
|
||||
Skills="groupSkills"
|
||||
IsMutating="IsMutating"
|
||||
CanEditGroup="CanEditCharacter(SelectedCharacter)"
|
||||
CanCreateSkill="CanEditCharacter(SelectedCharacter)"
|
||||
HasSkillFilter="hasSkillFilter"
|
||||
EmptyMessage="No skills in this group yet."
|
||||
ShowGroupActions="true"
|
||||
CanEditSkill="CanEditSkill"
|
||||
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||
AddSkillRequested="OnAddSkillRequestedAsync"
|
||||
EditSkillRequested="OnEditSkillRequestedAsync"
|
||||
RollSkillRequested="RollSkillAsync"
|
||||
DeleteSkillRequested="DeleteSkillAsync"
|
||||
EditGroupRequested="OnEditSkillGroupRequestedAsync"
|
||||
DeleteGroupRequested="DeleteSkillGroupAsync"/>
|
||||
}
|
||||
@if (!hasSkillFilter || ungroupedSkills.Count > 0)
|
||||
{
|
||||
<div class="skill-group-block">
|
||||
<div class="skill-group-head">
|
||||
<strong>Ungrouped</strong>
|
||||
</div>
|
||||
@if (!hasSkillFilter && ungroupedSkills.Count == 0)
|
||||
{
|
||||
<p class="empty">No ungrouped skills.</p>
|
||||
}
|
||||
<div class="skill-list">
|
||||
@foreach (var skill in ungroupedSkills)
|
||||
{
|
||||
<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(null)">
|
||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||
<span>Add skill</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SkillGroupBlock
|
||||
Title="Ungrouped"
|
||||
SkillGroupId="@((Guid?)null)"
|
||||
Skills="ungroupedSkills"
|
||||
IsMutating="IsMutating"
|
||||
CanEditGroup="false"
|
||||
CanCreateSkill="CanEditCharacter(SelectedCharacter)"
|
||||
HasSkillFilter="hasSkillFilter"
|
||||
EmptyMessage="No ungrouped skills."
|
||||
ShowGroupActions="false"
|
||||
CanEditSkill="CanEditSkill"
|
||||
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||
AddSkillRequested="OnAddSkillRequestedAsync"
|
||||
EditSkillRequested="OnEditSkillRequestedAsync"
|
||||
RollSkillRequested="RollSkillAsync"
|
||||
DeleteSkillRequested="DeleteSkillAsync"
|
||||
EditGroupRequested="OnEditSkillGroupRequestedAsync"
|
||||
DeleteGroupRequested="DeleteSkillGroupAsync"/>
|
||||
}
|
||||
|
||||
<button
|
||||
|
||||
@@ -72,6 +72,27 @@ public partial class CharacterPanel
|
||||
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()
|
||||
{
|
||||
SkillGroupState.Model.Name = string.Empty;
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -14,59 +14,19 @@
|
||||
}
|
||||
|
||||
<div class="workspace-shell">
|
||||
<header class="workspace-header">
|
||||
<div class="header-row">
|
||||
<h1>RpgRoller</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>
|
||||
}
|
||||
<p class="header-campaign">Campaign: <strong>@(SelectedCampaignName ?? "No campaign selected")</strong></p>
|
||||
<div class="header-connection-cell">
|
||||
<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>
|
||||
<AppHeader
|
||||
User="User"
|
||||
ShowCampaign="true"
|
||||
CampaignName="SelectedCampaignName"
|
||||
ShowConnectionState="true"
|
||||
ConnectionStateLabel="ConnectionStateLabel"
|
||||
ConnectionStateCssClass="ConnectionStateCssClass"
|
||||
IsMenuOpen="IsScreenMenuOpen"
|
||||
MenuButtonId="workspace-screen-menu-button"
|
||||
MenuId="workspace-screen-menu"
|
||||
MenuItems="HeaderMenuItems"
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="LogoutAsync"/>
|
||||
|
||||
@if (IsPlayScreen)
|
||||
{
|
||||
|
||||
@@ -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<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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user