< Summary

Information
Class: Chronicis.Client.Components.World.WorldMembersPanel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/World/WorldMembersPanel.razor
Line coverage
100%
Covered lines: 12
Uncovered lines: 0
Coverable lines: 12
Total lines: 377
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)100%66100%
.ctor()100%11100%
GetRoleColor(...)100%44100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/World/WorldMembersPanel.razor

#LineLine coverage
 1@using Chronicis.Shared.DTOs
 2@using Chronicis.Shared.Enums
 3@inject IWorldApiService WorldApi
 4@inject ISnackbar Snackbar
 5@inject IDialogService DialogService
 6@inject IJSRuntime JSRuntime
 7
 8<MudText Typo="Typo.h6" Class="mb-3" Style="color: var(--chronicis-beige-gold);">
 9    Members & Invitations
 10</MudText>
 11
 12<div class="mb-4 pa-3 rounded" style="background: var(--mud-palette-background-grey);">
 13    <!-- Members List -->
 14    <div class="d-flex align-center justify-space-between mb-2">
 15        <MudText Typo="Typo.subtitle1">Members (@_members.Count)</MudText>
 16    </div>
 17
 3918    @if (_members.Count == 0)
 19    {
 20        <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
 21            No members yet. Create an invitation to add players.
 22        </MudText>
 23    }
 24    else
 25    {
 26        <MudSimpleTable Dense="true" Hover="true" Class="mb-3">
 27            <thead>
 28                <tr>
 29                    <th>Member</th>
 30                    <th>Role</th>
 31                    <th>Joined</th>
 32                    <th style="width: 100px;">Actions</th>
 33                </tr>
 34            </thead>
 35            <tbody>
 36                @foreach (var member in _members)
 37                {
 38                    <tr>
 39                        <td>
 40                            <div class="d-flex align-center gap-2">
 41                                @if (!string.IsNullOrEmpty(member.AvatarUrl))
 42                                {
 43                                    <MudAvatar Size="Size.Small">
 44                                        <MudImage Src="@member.AvatarUrl" />
 45                                    </MudAvatar>
 46                                }
 47                                else
 48                                {
 49                                    <MudAvatar Size="Size.Small" Color="Color.Primary">
 50                                        @member.DisplayName[0]
 51                                    </MudAvatar>
 52                                }
 53                                <div>
 54                                    <MudText Typo="Typo.body2">@member.DisplayName</MudText>
 55                                    <MudText Typo="Typo.caption" Class="mud-text-secondary">@member.Email</MudText>
 56                                </div>
 57                            </div>
 58                        </td>
 59                        <td>
 60                            @if (IsCurrentUserGM && member.UserId != CurrentUserId)
 61                            {
 62                                <MudSelect T="WorldRole"
 63                                           Value="member.Role"
 64                                           ValueChanged="(role) => UpdateMemberRole(member, role)"
 65                                           Variant="Variant.Outlined"
 66                                           Margin="Margin.Dense"
 67                                           Dense="true"
 68                                           Style="min-width: 120px;">
 69                                    <MudSelectItem Value="WorldRole.GM">GM</MudSelectItem>
 70                                    <MudSelectItem Value="WorldRole.Player">Player</MudSelectItem>
 71                                    <MudSelectItem Value="WorldRole.Observer">Observer</MudSelectItem>
 72                                </MudSelect>
 73                            }
 74                            else
 75                            {
 76                                <MudChip T="string" Size="Size.Small" Color="@GetRoleColor(member.Role)">
 77                                    @member.Role
 78                                </MudChip>
 79                            }
 80                        </td>
 81                        <td>
 82                            <MudText Typo="Typo.body2">@member.JoinedAt.ToString("MMM d, yyyy")</MudText>
 83                        </td>
 84                        <td>
 85                            @if (IsCurrentUserGM && member.UserId != CurrentUserId)
 86                            {
 87                                <MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
 88                                               Color="Color.Error"
 89                                               Size="Size.Small"
 90                                               OnClick="() => RemoveMember(member)"
 91                                               title="Remove member" />
 92                            }
 93                        </td>
 94                    </tr>
 95                }
 96            </tbody>
 97        </MudSimpleTable>
 98    }
 99
 100    <!-- Invitations Section (GM only) -->
 39101    @if (IsCurrentUserGM)
 102    {
 103        <MudDivider Class="my-3" />
 104
 105        <div class="d-flex align-center justify-space-between mb-2">
 106            <MudText Typo="Typo.subtitle1">Invitations</MudText>
 107            <MudButton Variant="Variant.Filled"
 108                       Color="Color.Primary"
 109                       Size="Size.Small"
 110                       StartIcon="@Icons.Material.Filled.PersonAdd"
 111                       OnClick="CreateInvitation"
 112                       Disabled="_isCreatingInvitation">
 113                @if (_isCreatingInvitation)
 114                {
 115                    <MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
 116                }
 117                Create Invitation
 118            </MudButton>
 119        </div>
 120
 37121        @if (_invitations.Count == 0)
 122        {
 123            <MudText Typo="Typo.body2" Class="mud-text-secondary">
 124                No active invitations. Create one to invite players to your world.
 125            </MudText>
 126        }
 127        else
 128        {
 129            <MudSimpleTable Dense="true" Hover="true">
 130                <thead>
 131                    <tr>
 132                        <th>Code</th>
 133                        <th>Role</th>
 134                        <th>Uses</th>
 135                        <th>Expires</th>
 136                        <th style="width: 100px;">Actions</th>
 137                    </tr>
 138                </thead>
 139                <tbody>
 140                    @foreach (var inv in _invitations.Where(i => i.IsActive))
 141                    {
 142                        <tr>
 143                            <td>
 144                                <div class="d-flex align-center gap-1">
 145                                    <code style="font-size: 1.1em; letter-spacing: 1px;">@inv.Code</code>
 146                                    <MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
 147                                                   Size="Size.Small"
 148                                                   OnClick="() => CopyInvitationCode(inv.Code)"
 149                                                   title="Copy code" />
 150                                </div>
 151                            </td>
 152                            <td>
 153                                <MudChip T="string" Size="Size.Small" Color="@GetRoleColor(inv.Role)">
 154                                    @inv.Role
 155                                </MudChip>
 156                            </td>
 157                            <td>
 158                                @if (inv.MaxUses.HasValue)
 159                                {
 160                                    <span>@inv.UsedCount / @inv.MaxUses</span>
 161                                }
 162                                else
 163                                {
 164                                    <span>@inv.UsedCount / âˆž</span>
 165                                }
 166                            </td>
 167                            <td>
 168                                @if (inv.ExpiresAt.HasValue)
 169                                {
 170                                    <span>@inv.ExpiresAt.Value.ToString("MMM d, yyyy")</span>
 171                                }
 172                                else
 173                                {
 174                                    <span class="mud-text-secondary">Never</span>
 175                                }
 176                            </td>
 177                            <td>
 178                                <MudIconButton Icon="@Icons.Material.Filled.Delete"
 179                                               Color="Color.Error"
 180                                               Size="Size.Small"
 181                                               OnClick="() => RevokeInvitation(inv)"
 182                                               title="Revoke invitation" />
 183                            </td>
 184                        </tr>
 185                    }
 186                </tbody>
 187            </MudSimpleTable>
 188        }
 189    }
 190</div>
 191
 192@code {
 193    [Parameter]
 194    public Guid WorldId { get; set; }
 195
 196    [Parameter]
 197    public Guid CurrentUserId { get; set; }
 198
 199    [Parameter]
 200    public bool IsCurrentUserGM { get; set; }
 201
 202    [Parameter]
 203    public EventCallback OnMembersChanged { get; set; }
 204
 23205    private List<WorldMemberDto> _members = new();
 23206    private List<WorldInvitationDto> _invitations = new();
 207    private bool _isCreatingInvitation = false;
 208
 209    protected override async Task OnParametersSetAsync()
 210    {
 211        await LoadDataAsync();
 212    }
 213
 214    private async Task LoadDataAsync()
 215    {
 216        try
 217        {
 218            _members = await WorldApi.GetMembersAsync(WorldId);
 219
 220            if (IsCurrentUserGM)
 221            {
 222                _invitations = await WorldApi.GetInvitationsAsync(WorldId);
 223            }
 224        }
 225        catch (Exception ex)
 226        {
 227            Snackbar.Add($"Failed to load members: {ex.Message}", Severity.Error);
 228        }
 229    }
 230
 231    private async Task UpdateMemberRole(WorldMemberDto member, WorldRole newRole)
 232    {
 233        try
 234        {
 235            var dto = new WorldMemberUpdateDto { Role = newRole };
 236            var updated = await WorldApi.UpdateMemberRoleAsync(WorldId, member.Id, dto);
 237
 238            if (updated != null)
 239            {
 240                member.Role = updated.Role;
 241                Snackbar.Add($"Updated {member.DisplayName}'s role to {newRole}", Severity.Success);
 242                await OnMembersChanged.InvokeAsync();
 243            }
 244            else
 245            {
 246                Snackbar.Add("Failed to update role. Cannot demote the last GM.", Severity.Warning);
 247                await LoadDataAsync(); // Refresh to get correct state
 248            }
 249        }
 250        catch (Exception ex)
 251        {
 252            Snackbar.Add($"Failed to update role: {ex.Message}", Severity.Error);
 253        }
 254
 255        StateHasChanged();
 256    }
 257
 258    private async Task RemoveMember(WorldMemberDto member)
 259    {
 260        var confirmed = await DialogService.ShowMessageBox(
 261            "Remove Member",
 262            $"Are you sure you want to remove {member.DisplayName} from this world?",
 263            yesText: "Remove",
 264            cancelText: "Cancel");
 265
 266        if (confirmed != true) return;
 267
 268        try
 269        {
 270            var success = await WorldApi.RemoveMemberAsync(WorldId, member.Id);
 271
 272            if (success)
 273            {
 274                _members.Remove(member);
 275                Snackbar.Add($"Removed {member.DisplayName} from the world", Severity.Success);
 276                await OnMembersChanged.InvokeAsync();
 277                StateHasChanged();
 278            }
 279            else
 280            {
 281                Snackbar.Add("Failed to remove member. Cannot remove the last GM.", Severity.Warning);
 282            }
 283        }
 284        catch (Exception ex)
 285        {
 286            Snackbar.Add($"Failed to remove member: {ex.Message}", Severity.Error);
 287        }
 288    }
 289
 290    private async Task CreateInvitation()
 291    {
 292        _isCreatingInvitation = true;
 293        StateHasChanged();
 294
 295        try
 296        {
 297            var dto = new WorldInvitationCreateDto
 298            {
 299                Role = WorldRole.Player // Default to Player
 300            };
 301
 302            var invitation = await WorldApi.CreateInvitationAsync(WorldId, dto);
 303
 304            if (invitation != null)
 305            {
 306                _invitations.Insert(0, invitation);
 307                Snackbar.Add($"Invitation created: {invitation.Code}", Severity.Success);
 308                await CopyInvitationCode(invitation.Code);
 309            }
 310            else
 311            {
 312                Snackbar.Add("Failed to create invitation", Severity.Error);
 313            }
 314        }
 315        catch (Exception ex)
 316        {
 317            Snackbar.Add($"Failed to create invitation: {ex.Message}", Severity.Error);
 318        }
 319        finally
 320        {
 321            _isCreatingInvitation = false;
 322            StateHasChanged();
 323        }
 324    }
 325
 326    private async Task RevokeInvitation(WorldInvitationDto invitation)
 327    {
 328        var confirmed = await DialogService.ShowMessageBox(
 329            "Revoke Invitation",
 330            $"Are you sure you want to revoke invitation {invitation.Code}?",
 331            yesText: "Revoke",
 332            cancelText: "Cancel");
 333
 334        if (confirmed != true) return;
 335
 336        try
 337        {
 338            var success = await WorldApi.RevokeInvitationAsync(WorldId, invitation.Id);
 339
 340            if (success)
 341            {
 342                invitation.IsActive = false;
 343                Snackbar.Add("Invitation revoked", Severity.Success);
 344                StateHasChanged();
 345            }
 346            else
 347            {
 348                Snackbar.Add("Failed to revoke invitation", Severity.Error);
 349            }
 350        }
 351        catch (Exception ex)
 352        {
 353            Snackbar.Add($"Failed to revoke invitation: {ex.Message}", Severity.Error);
 354        }
 355    }
 356
 357    private async Task CopyInvitationCode(string code)
 358    {
 359        try
 360        {
 361            await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", code);
 362            Snackbar.Add($"Copied {code} to clipboard", Severity.Success);
 363        }
 364        catch
 365        {
 366            Snackbar.Add("Failed to copy code", Severity.Error);
 367        }
 368    }
 369
 17370    private static Color GetRoleColor(WorldRole role) => role switch
 17371    {
 1372        WorldRole.GM => Color.Warning,
 13373        WorldRole.Player => Color.Primary,
 2374        WorldRole.Observer => Color.Default,
 1375        _ => Color.Default
 17376    };
 377}