< 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
0%
Covered lines: 0
Uncovered lines: 124
Coverable lines: 124
Total lines: 377
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 46
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)0%4260%
get_WorldId()100%210%
get_CurrentUserId()100%210%
get_IsCurrentUserGM()100%210%
get_OnMembersChanged()100%210%
.ctor()100%210%
OnParametersSetAsync()100%210%
LoadDataAsync()0%620%
UpdateMemberRole()0%620%
RemoveMember()0%4260%
CreateInvitation()0%620%
RevokeInvitation()0%2040%
CopyInvitationCode()100%210%
GetRoleColor(...)0%2040%

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">
 015        <MudText Typo="Typo.subtitle1">Members (@_members.Count)</MudText>
 16    </div>
 17
 018    @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>
 036                @foreach (var member in _members)
 37                {
 38                    <tr>
 39                        <td>
 40                            <div class="d-flex align-center gap-2">
 041                                @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">
 050                                        @member.DisplayName[0]
 51                                    </MudAvatar>
 52                                }
 53                                <div>
 054                                    <MudText Typo="Typo.body2">@member.DisplayName</MudText>
 055                                    <MudText Typo="Typo.caption" Class="mud-text-secondary">@member.Email</MudText>
 56                                </div>
 57                            </div>
 58                        </td>
 59                        <td>
 060                            @if (IsCurrentUserGM && member.UserId != CurrentUserId)
 61                            {
 62                                <MudSelect T="WorldRole"
 63                                           Value="member.Role"
 064                                           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)">
 077                                    @member.Role
 78                                </MudChip>
 79                            }
 80                        </td>
 81                        <td>
 082                            <MudText Typo="Typo.body2">@member.JoinedAt.ToString("MMM d, yyyy")</MudText>
 83                        </td>
 84                        <td>
 085                            @if (IsCurrentUserGM && member.UserId != CurrentUserId)
 86                            {
 87                                <MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
 88                                               Color="Color.Error"
 89                                               Size="Size.Small"
 090                                               OnClick="() => RemoveMember(member)"
 91                                               title="Remove member" />
 92                            }
 93                        </td>
 94                    </tr>
 95                }
 96            </tbody>
 97        </MudSimpleTable>
 98    }
 99
 100    <!-- Invitations Section (GM only) -->
 0101    @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">
 0113                @if (_isCreatingInvitation)
 114                {
 115                    <MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
 116                }
 117                Create Invitation
 118            </MudButton>
 119        </div>
 120
 0121        @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>
 0140                    @foreach (var inv in _invitations.Where(i => i.IsActive))
 141                    {
 142                        <tr>
 143                            <td>
 144                                <div class="d-flex align-center gap-1">
 0145                                    <code style="font-size: 1.1em; letter-spacing: 1px;">@inv.Code</code>
 146                                    <MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
 147                                                   Size="Size.Small"
 0148                                                   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)">
 0154                                    @inv.Role
 155                                </MudChip>
 156                            </td>
 157                            <td>
 0158                                @if (inv.MaxUses.HasValue)
 159                                {
 0160                                    <span>@inv.UsedCount / @inv.MaxUses</span>
 161                                }
 162                                else
 163                                {
 0164                                    <span>@inv.UsedCount / âˆž</span>
 165                                }
 166                            </td>
 167                            <td>
 0168                                @if (inv.ExpiresAt.HasValue)
 169                                {
 0170                                    <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"
 0181                                               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]
 0194    public Guid WorldId { get; set; }
 195
 196    [Parameter]
 0197    public Guid CurrentUserId { get; set; }
 198
 199    [Parameter]
 0200    public bool IsCurrentUserGM { get; set; }
 201
 202    [Parameter]
 0203    public EventCallback OnMembersChanged { get; set; }
 204
 0205    private List<WorldMemberDto> _members = new();
 0206    private List<WorldInvitationDto> _invitations = new();
 207    private bool _isCreatingInvitation = false;
 208
 209    protected override async Task OnParametersSetAsync()
 210    {
 0211        await LoadDataAsync();
 0212    }
 213
 214    private async Task LoadDataAsync()
 215    {
 216        try
 217        {
 0218            _members = await WorldApi.GetMembersAsync(WorldId);
 219
 0220            if (IsCurrentUserGM)
 221            {
 0222                _invitations = await WorldApi.GetInvitationsAsync(WorldId);
 223            }
 0224        }
 0225        catch (Exception ex)
 226        {
 0227            Snackbar.Add($"Failed to load members: {ex.Message}", Severity.Error);
 0228        }
 0229    }
 230
 231    private async Task UpdateMemberRole(WorldMemberDto member, WorldRole newRole)
 232    {
 233        try
 234        {
 0235            var dto = new WorldMemberUpdateDto { Role = newRole };
 0236            var updated = await WorldApi.UpdateMemberRoleAsync(WorldId, member.Id, dto);
 237
 0238            if (updated != null)
 239            {
 0240                member.Role = updated.Role;
 0241                Snackbar.Add($"Updated {member.DisplayName}'s role to {newRole}", Severity.Success);
 0242                await OnMembersChanged.InvokeAsync();
 243            }
 244            else
 245            {
 0246                Snackbar.Add("Failed to update role. Cannot demote the last GM.", Severity.Warning);
 0247                await LoadDataAsync(); // Refresh to get correct state
 248            }
 0249        }
 0250        catch (Exception ex)
 251        {
 0252            Snackbar.Add($"Failed to update role: {ex.Message}", Severity.Error);
 0253        }
 254
 0255        StateHasChanged();
 0256    }
 257
 258    private async Task RemoveMember(WorldMemberDto member)
 259    {
 0260        var confirmed = await DialogService.ShowMessageBox(
 0261            "Remove Member",
 0262            $"Are you sure you want to remove {member.DisplayName} from this world?",
 0263            yesText: "Remove",
 0264            cancelText: "Cancel");
 265
 0266        if (confirmed != true) return;
 267
 268        try
 269        {
 0270            var success = await WorldApi.RemoveMemberAsync(WorldId, member.Id);
 271
 0272            if (success)
 273            {
 0274                _members.Remove(member);
 0275                Snackbar.Add($"Removed {member.DisplayName} from the world", Severity.Success);
 0276                await OnMembersChanged.InvokeAsync();
 0277                StateHasChanged();
 278            }
 279            else
 280            {
 0281                Snackbar.Add("Failed to remove member. Cannot remove the last GM.", Severity.Warning);
 282            }
 0283        }
 0284        catch (Exception ex)
 285        {
 0286            Snackbar.Add($"Failed to remove member: {ex.Message}", Severity.Error);
 0287        }
 0288    }
 289
 290    private async Task CreateInvitation()
 291    {
 0292        _isCreatingInvitation = true;
 0293        StateHasChanged();
 294
 295        try
 296        {
 0297            var dto = new WorldInvitationCreateDto
 0298            {
 0299                Role = WorldRole.Player // Default to Player
 0300            };
 301
 0302            var invitation = await WorldApi.CreateInvitationAsync(WorldId, dto);
 303
 0304            if (invitation != null)
 305            {
 0306                _invitations.Insert(0, invitation);
 0307                Snackbar.Add($"Invitation created: {invitation.Code}", Severity.Success);
 0308                await CopyInvitationCode(invitation.Code);
 309            }
 310            else
 311            {
 0312                Snackbar.Add("Failed to create invitation", Severity.Error);
 313            }
 0314        }
 0315        catch (Exception ex)
 316        {
 0317            Snackbar.Add($"Failed to create invitation: {ex.Message}", Severity.Error);
 0318        }
 319        finally
 320        {
 0321            _isCreatingInvitation = false;
 0322            StateHasChanged();
 323        }
 0324    }
 325
 326    private async Task RevokeInvitation(WorldInvitationDto invitation)
 327    {
 0328        var confirmed = await DialogService.ShowMessageBox(
 0329            "Revoke Invitation",
 0330            $"Are you sure you want to revoke invitation {invitation.Code}?",
 0331            yesText: "Revoke",
 0332            cancelText: "Cancel");
 333
 0334        if (confirmed != true) return;
 335
 336        try
 337        {
 0338            var success = await WorldApi.RevokeInvitationAsync(WorldId, invitation.Id);
 339
 0340            if (success)
 341            {
 0342                invitation.IsActive = false;
 0343                Snackbar.Add("Invitation revoked", Severity.Success);
 0344                StateHasChanged();
 345            }
 346            else
 347            {
 0348                Snackbar.Add("Failed to revoke invitation", Severity.Error);
 349            }
 0350        }
 0351        catch (Exception ex)
 352        {
 0353            Snackbar.Add($"Failed to revoke invitation: {ex.Message}", Severity.Error);
 0354        }
 0355    }
 356
 357    private async Task CopyInvitationCode(string code)
 358    {
 359        try
 360        {
 0361            await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", code);
 0362            Snackbar.Add($"Copied {code} to clipboard", Severity.Success);
 0363        }
 0364        catch
 365        {
 0366            Snackbar.Add("Failed to copy code", Severity.Error);
 0367        }
 0368    }
 369
 0370    private static Color GetRoleColor(WorldRole role) => role switch
 0371    {
 0372        WorldRole.GM => Color.Warning,
 0373        WorldRole.Player => Color.Primary,
 0374        WorldRole.Observer => Color.Default,
 0375        _ => Color.Default
 0376    };
 377}