< Summary

Information
Class: Chronicis.Client.Pages.WorldDetail
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/WorldDetail.razor
Line coverage
100%
Covered lines: 8
Uncovered lines: 0
Coverable lines: 8
Total lines: 451
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
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%44100%
OnViewModelChanged(...)100%11100%
Dispose()100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/WorldDetail.razor

#LineLine coverage
 1@attribute [Authorize]
 2@implements IDisposable
 3@using Chronicis.Client.Components.Dialogs
 4@using Chronicis.Client.Components.Shared
 5@using Chronicis.Client.Components.World
 6@using Chronicis.Client.Components.Settings
 7@inject WorldDetailViewModel ViewModel
 8@inject WorldLinksViewModel LinksViewModel
 9@inject WorldDocumentsViewModel DocumentsViewModel
 10@inject WorldSharingViewModel SharingViewModel
 11@inject IAppContextService AppContext
 12@inject IJSRuntime JSRuntime
 13@inject NavigationManager Navigation
 14
 315@if (ViewModel.IsLoading)
 16{
 17    <LoadingSkeleton />
 18}
 219else if (ViewModel.World != null)
 20{
 21    <MudPaper Elevation="2" Class="chronicis-article-card chronicis-fade-in">
 22        <DetailPageHeader Breadcrumbs="ViewModel.Breadcrumbs"
 23                          Icon="@Icons.Material.Filled.Public"
 24                          Title="@ViewModel.EditName"
 25                          TitleChanged="@(v => ViewModel.EditName = v)"
 26                          Placeholder="World Name"
 27                          OnTitleEdited="@(() => {})"
 28                          OnEnterPressed="@(() => ViewModel.SaveAsync(SharingViewModel))"
 29                          ReadOnly="@(!ViewModel.CanManageWorldDetails)" />
 30
 31        <!-- Description -->
 32        <MudTextField Value="@ViewModel.EditDescription"
 33                      ValueChanged="@((string v) => ViewModel.EditDescription = v)"
 34                      Label="Description"
 35                      Variant="Variant.Outlined"
 36                      Lines="5"
 37                      AutoGrow="true"
 38                      MaxLines="20"
 39                      Placeholder="Describe your world..."
 40                      Class="mb-4"
 41                      Immediate="true"
 42                      ReadOnly="@(!ViewModel.CanManageWorldDetails)" />
 43
 44        <!-- Overview Section: Stats + Quick Actions -->
 45        <div class="world-overview mb-4">
 46            <div class="world-stats-grid">
 47                <div class="stat-card">
 48                    <MudIcon Icon="@Icons.Material.Filled.AutoStories" Class="stat-icon" />
 49                    <div class="stat-content">
 50                        <div class="stat-value">@ViewModel.World.CampaignCount</div>
 51                        <div class="stat-label">Campaigns</div>
 52                    </div>
 53                </div>
 54                <div class="stat-card">
 55                    <MudIcon Icon="@Icons.Material.Filled.Group" Class="stat-icon" />
 56                    <div class="stat-content">
 57                        <div class="stat-value">@ViewModel.World.MemberCount</div>
 58                        <div class="stat-label">Members</div>
 59                    </div>
 60                </div>
 61                <div class="stat-card">
 62                    <MudIcon Icon="@Icons.Material.Filled.CalendarToday" Class="stat-icon" />
 63                    <div class="stat-content">
 64                        <div class="stat-value">@ViewModel.World.CreatedAt.ToString("MMM yyyy")</div>
 65                        <div class="stat-label">Created</div>
 66                    </div>
 67                </div>
 68            </div>
 69
 70            <div class="quick-actions-compact">
 71                @if (ViewModel.CanManageWorldDetails)
 72                {
 73                    <MudButton Variant="Variant.Filled"
 74                               Color="Color.Primary"
 75                               StartIcon="@Icons.Material.Filled.AutoStories"
 76                               OnClick="@(() => ViewModel.CreateCampaignAsync())"
 77                               Size="Size.Small">
 78                        New Campaign
 79                    </MudButton>
 80                }
 81                <MudButton Variant="Variant.Filled"
 82                           Color="Color.Primary"
 83                           StartIcon="@Icons.Material.Filled.Person"
 84                           OnClick="@(() => ViewModel.CreateCharacterAsync())"
 85                           Size="Size.Small">
 86                    New Character
 87                </MudButton>
 88                <MudButton Variant="Variant.Filled"
 89                           Color="Color.Primary"
 90                           StartIcon="@Icons.Material.Filled.MenuBook"
 91                           OnClick="@(() => ViewModel.CreateWikiArticleAsync())"
 92                           Size="Size.Small">
 93                    New Article
 94                </MudButton>
 95            </div>
 96        </div>
 97
 98        <!-- Tabbed Content -->
 99        <MudTabs Elevation="0"
 100                 Rounded="true"
 101                 ApplyEffectsToContainer="true"
 102                 PanelClass="pa-4">
 103
 104            <!-- Resources Tab -->
 105            <MudTabPanel Text="Resources" Icon="@Icons.Material.Filled.Folder">
 106                <div class="resources-section">
 107                    <!-- Links Subsection -->
 108                    <div class="resource-subsection mb-4">
 109                        <div class="d-flex align-center justify-space-between mb-2">
 110                            <MudText Typo="Typo.subtitle1" Style="font-weight: 600;">External Links</MudText>
 111                            @if (ViewModel.CanManageWorldDetails)
 112                            {
 113                                <MudButton Variant="Variant.Text"
 114                                           Color="Color.Primary"
 115                                           StartIcon="@Icons.Material.Filled.Add"
 116                                           OnClick="@(() => LinksViewModel.StartAddLink())"
 117                                           Size="Size.Small">
 118                                    Add Link
 119                                </MudButton>
 120                            }
 121                        </div>
 122
 123                        @if (LinksViewModel.Links.Count == 0 && !LinksViewModel.IsAddingLink)
 124                        {
 125                            <MudText Typo="Typo.body2" Class="mud-text-secondary">
 126                                No external links yet. Add links to Roll20, D&amp;D Beyond, or other resources.
 127                            </MudText>
 128                        }
 129
 130                        @foreach (var link in LinksViewModel.Links)
 131                        {
 132                            <div class="resource-item">
 133                                @if (LinksViewModel.EditingLinkId == link.Id)
 134                                {
 135                                    <MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Small" Style="color: var(--ch
 136                                    <MudTextField Value="@LinksViewModel.EditLinkTitle"
 137                                                  ValueChanged="@((string v) => LinksViewModel.EditLinkTitle = v)"
 138                                                  Placeholder="Title" Variant="Variant.Outlined" Margin="Margin.Dense" S
 139                                    <MudTextField Value="@LinksViewModel.EditLinkUrl"
 140                                                  ValueChanged="@((string v) => LinksViewModel.EditLinkUrl = v)"
 141                                                  Placeholder="URL" Variant="Variant.Outlined" Margin="Margin.Dense" Cla
 142                                    <MudTextField Value="@LinksViewModel.EditLinkDescription"
 143                                                  ValueChanged="@((string v) => LinksViewModel.EditLinkDescription = v)"
 144                                                  Placeholder="Description (optional)" Variant="Variant.Outlined" Margin
 145                                    <MudIconButton Icon="@Icons.Material.Filled.Check" Color="Color.Success" Size="Size.
 146                                                   OnClick="@(() => LinksViewModel.SaveEditLinkAsync())" Disabled="Links
 147                                    <MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Default" Size="Size.
 148                                                   OnClick="@(() => LinksViewModel.CancelEditLink())" />
 149                                }
 150                                else
 151                                {
 152                                    <img src="@WorldLinksViewModel.GetFaviconUrl(link.Url)" alt="" style="width: 20px; h
 153                                    <div class="flex-grow-1">
 154                                        <MudLink Href="@link.Url" Target="_blank" Typo="Typo.body1" Style="color: var(--
 155                                            @link.Title
 156                                            <MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" Style="fo
 157                                        </MudLink>
 158                                        @if (!string.IsNullOrWhiteSpace(link.Description))
 159                                        {
 160                                            <MudText Typo="Typo.caption" Class="mud-text-secondary">@link.Description</M
 161                                        }
 162                                    </div>
 163                                    @if (ViewModel.CanManageWorldDetails)
 164                                    {
 165                                        <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" Size="Si
 166                                        <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Si
 167                                    }
 168                                }
 169                            </div>
 170                        }
 171
 172                        @if (LinksViewModel.IsAddingLink)
 173                        {
 174                            <div class="resource-item">
 175                                <MudIcon Icon="@Icons.Material.Filled.AddLink" Size="Size.Small" Style="color: var(--chr
 176                                <MudTextField Value="@LinksViewModel.NewLinkTitle"
 177                                              ValueChanged="@((string v) => LinksViewModel.NewLinkTitle = v)"
 178                                              Placeholder="Title (e.g., Roll20 Campaign)" Variant="Variant.Outlined" Mar
 179                                <MudTextField Value="@LinksViewModel.NewLinkUrl"
 180                                              ValueChanged="@((string v) => LinksViewModel.NewLinkUrl = v)"
 181                                              Placeholder="URL (e.g., https://roll20.net/...)" Variant="Variant.Outlined
 182                                <MudTextField Value="@LinksViewModel.NewLinkDescription"
 183                                              ValueChanged="@((string v) => LinksViewModel.NewLinkDescription = v)"
 184                                              Placeholder="Description (optional)" Variant="Variant.Outlined" Margin="Ma
 185                                <MudIconButton Icon="@Icons.Material.Filled.Check" Color="Color.Success" Size="Size.Smal
 186                                               OnClick="@(() => LinksViewModel.SaveNewLinkAsync())" Disabled="LinksViewM
 187                                <MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Default" Size="Size.Smal
 188                                               OnClick="@(() => LinksViewModel.CancelAddLink())" />
 189                            </div>
 190                        }
 191                    </div>
 192
 193                    <MudDivider Class="my-4" />
 194
 195                    <!-- Documents Subsection -->
 196                    <div class="resource-subsection">
 197                        <div class="d-flex align-center justify-space-between mb-2">
 198                            <MudText Typo="Typo.subtitle1" Style="font-weight: 600;">Documents</MudText>
 199                            @if (ViewModel.IsCurrentUserGm)
 200                            {
 201                                <MudButton Variant="Variant.Text"
 202                                           Color="Color.Primary"
 203                                           StartIcon="@Icons.Material.Filled.Upload"
 204                                           OnClick="@(() => DocumentsViewModel.OpenUploadDialogAsync(WorldId))"
 205                                           Size="Size.Small">
 206                                    Upload
 207                                </MudButton>
 208                            }
 209                        </div>
 210
 211                        @if (DocumentsViewModel.Documents.Count == 0)
 212                        {
 213                            <MudText Typo="Typo.body2" Class="mud-text-secondary">
 214                                No documents uploaded yet. Upload PDFs, Office files, images, or other documents.
 215                            </MudText>
 216                        }
 217                        else
 218                        {
 219                            @foreach (var doc in DocumentsViewModel.Documents.OrderBy(d => d.Title))
 220                            {
 221                                <div class="resource-item">
 222                                    @if (DocumentsViewModel.EditingDocumentId == doc.Id)
 223                                    {
 224                                        <MudIcon Icon="@WorldDocumentsViewModel.GetDocumentIcon(doc.ContentType)" Size="
 225                                        <MudTextField Value="@DocumentsViewModel.EditDocumentTitle"
 226                                                      ValueChanged="@((string v) => DocumentsViewModel.EditDocumentTitle
 227                                                      Placeholder="Title" Variant="Variant.Outlined" Margin="Margin.Dens
 228                                        <MudTextField Value="@DocumentsViewModel.EditDocumentDescription"
 229                                                      ValueChanged="@((string v) => DocumentsViewModel.EditDocumentDescr
 230                                                      Placeholder="Description (optional)" Variant="Variant.Outlined" Ma
 231                                        <MudIconButton Icon="@Icons.Material.Filled.Check" Color="Color.Success" Size="S
 232                                                       OnClick="@(() => DocumentsViewModel.SaveDocumentEditAsync())" Dis
 233                                        <MudIconButton Icon="@Icons.Material.Filled.Close" Color="Color.Default" Size="S
 234                                                       OnClick="@(() => DocumentsViewModel.CancelDocumentEdit())" />
 235                                    }
 236                                    else
 237                                    {
 238                                        <MudIcon Icon="@WorldDocumentsViewModel.GetDocumentIcon(doc.ContentType)" Size="
 239                                        <div style="flex: 1; min-width: 0;">
 240                                            <MudText Typo="Typo.body2" Style="font-weight: 500;">@doc.Title</MudText>
 241                                            @if (!string.IsNullOrEmpty(doc.Description))
 242                                            {
 243                                                <MudText Typo="Typo.caption" Class="mud-text-secondary">@doc.Description
 244                                            }
 245                                            <MudText Typo="Typo.caption" Class="mud-text-secondary">
 246                                                @WorldDocumentsViewModel.FormatFileSize(doc.FileSizeBytes) â€¢ Uploaded @d
 247                                            </MudText>
 248                                        </div>
 249                                        <MudIconButton Icon="@Icons.Material.Filled.Download" Color="Color.Primary" Size
 250                                                       OnClick="@(() => DocumentsViewModel.DownloadDocumentAsync(doc.Id)
 251                                        @if (ViewModel.IsCurrentUserGm)
 252                                        {
 253                                            <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" Size
 254                                                           OnClick="@(() => DocumentsViewModel.StartEditDocument(doc))" 
 255                                            <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size
 256                                                           OnClick="@(() => DocumentsViewModel.DeleteDocumentAsync(doc.I
 257                                        }
 258                                    }
 259                                </div>
 260                            }
 261                        }
 262                    </div>
 263                </div>
 264            </MudTabPanel>
 265
 266            <!-- Members & Sharing Tab -->
 267            <MudTabPanel Text="Members &amp; Sharing" Icon="@Icons.Material.Filled.People">
 268                <WorldMembersPanel WorldId="WorldId"
 269                                   CurrentUserId="ViewModel.CurrentUserId"
 270                                   IsCurrentUserGM="ViewModel.IsCurrentUserGm"
 271                                   OnMembersChanged="@(() => ViewModel.OnMembersChangedAsync())" />
 272
 273                <MudDivider Class="my-4" />
 274
 275                <!-- Public Sharing Section -->
 276                <div class="sharing-section">
 277                    <MudText Typo="Typo.subtitle1" Class="mb-3" Style="font-weight: 600;">Public Sharing</MudText>
 278
 279                    <div class="pa-3 rounded mb-4" style="background: var(--mud-palette-background-grey);">
 280                        <div class="d-flex align-center gap-3 mb-2">
 281                            <MudSwitch Value="@SharingViewModel.IsPublic"
 282                                       ValueChanged="@((bool v) => { SharingViewModel.IsPublic = v; SharingViewModel.OnP
 283                                       Color="Color.Primary"
 284                                       Label="Make this world publicly accessible"
 285                                       T="bool"
 286                                       Disabled="@(!ViewModel.CanManageWorldDetails)" />
 287                        </div>
 288
 289                        @if (SharingViewModel.IsPublic)
 290                        {
 291                            <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
 292                                Anyone with the link can view articles marked as "Public". Members-only and private arti
 293                            </MudText>
 294
 295                            @if (SharingViewModel.ShouldShowPublicPreview(ViewModel.World))
 296                            {
 297                                <div class="d-flex align-center gap-2">
 298                                    <MudTextField Value="@SharingViewModel.GetFullPublicUrl(Navigation.BaseUri, ViewMode
 299                                                  Label="Public URL"
 300                                                  Variant="Variant.Outlined"
 301                                                  Margin="Margin.Dense"
 302                                                  ReadOnly="true"
 303                                                  Adornment="Adornment.End"
 304                                                  AdornmentIcon="@Icons.Material.Filled.ContentCopy"
 305                                                  OnAdornmentClick="CopyPublicUrl"
 306                                                  Style="max-width: 500px;" />
 307                                    <MudButton Variant="Variant.Outlined"
 308                                               Color="Color.Primary"
 309                                               StartIcon="@Icons.Material.Filled.OpenInNew"
 310                                               Href="@SharingViewModel.GetFullPublicUrl(Navigation.BaseUri, ViewModel.Wo
 311                                               Target="_blank">
 312                                        Preview
 313                                    </MudButton>
 314                                </div>
 315                            }
 316                        }
 317                        else
 318                        {
 319                            <MudText Typo="Typo.body2" Class="mud-text-secondary">
 320                                Your world is currently private. Only you and campaign members can access it.
 321                            </MudText>
 322                        }
 323                    </div>
 324
 325                    <MudText Typo="Typo.subtitle1" Class="mb-3" Style="font-weight: 600;">World URL</MudText>
 326
 327                    <div class="pa-3 rounded" style="background: var(--mud-palette-background-grey);">
 328                        <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
 329                            Your world's URL segment. Changing it updates all public links to this world.
 330                        </MudText>
 331                        <div class="d-flex align-center gap-2">
 332                            <MudText Typo="Typo.body2" Style="white-space: nowrap;">@SharingViewModel.GetPublicUrlBase(N
 333                            <MudTextField @bind-Value="SharingViewModel.PendingSlug"
 334                                          Placeholder="your-world-slug"
 335                                          Variant="Variant.Outlined"
 336                                          Margin="Margin.Dense"
 337                                          Style="max-width: 300px;"
 338                                          Disabled="@(SharingViewModel.IsRenamingSlug || !ViewModel.CanManageWorldDetail
 339                                          Error="@(!string.IsNullOrEmpty(SharingViewModel.SlugRenameError))"
 340                                          ErrorText="@SharingViewModel.SlugRenameError" />
 341                            <MudButton Variant="Variant.Outlined"
 342                                       Color="Color.Primary"
 343                                       OnClick="SaveWorldSlug"
 344                                       Disabled="@(SharingViewModel.IsRenamingSlug || !ViewModel.CanManageWorldDetails)"
 345                                @if (SharingViewModel.IsRenamingSlug)
 346                                {
 347                                    <MudProgressCircular Size="Size.Small" Indeterminate="true" />
 348                                }
 349                                else
 350                                {
 351                                    <MudText>Save URL</MudText>
 352                                }
 353                            </MudButton>
 354                        </div>
 355                    </div>
 356                </div>
 357            </MudTabPanel>
 358
 359            @if (ViewModel.CanViewPrivateNotes)
 360            {
 361                <MudTabPanel Text="Private Notes" Icon="@Icons.Material.Filled.Lock">
 362                    <MudText Typo="Typo.subtitle1" Class="mb-2" Style="font-weight: 600;">
 363                        World Private Notes
 364                    </MudText>
 365                    <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
 366                        Visible only to the world owner and GMs.
 367                    </MudText>
 368                    <PrivateNotesTipTapEditor WorldId="WorldId"
 369                                             Value="@ViewModel.EditPrivateNotes"
 370                                             ValueChanged="@((string v) => ViewModel.EditPrivateNotes = v)"
 371                                             ReadOnly="@(!ViewModel.CanManageWorldDetails)"
 372                                             UploadContextLabel="world private notes" />
 373                </MudTabPanel>
 374            }
 375
 376            <!-- Settings Tab -->
 377            <MudTabPanel Text="Settings" Icon="@Icons.Material.Filled.Settings">
 378                @if (ViewModel.IsCurrentUserGm)
 379                {
 380                    <WorldResourceProviders WorldId="WorldId" />
 381                }
 382                else
 383                {
 384                    <MudAlert Severity="Severity.Info">
 385                        Only the world owner can manage settings.
 386                    </MudAlert>
 387                }
 388            </MudTabPanel>
 389        </MudTabs>
 390
 391        <!-- Save Status -->
 392        <div class="chronicis-flex-between mt-4">
 393            <SaveStatusIndicator IsSaving="ViewModel.IsSaving" HasUnsavedChanges="ViewModel.HasUnsavedChanges" />
 394            <MudButton Variant="Variant.Filled"
 395                       Color="Color.Primary"
 396                       OnClick="@(() => ViewModel.SaveAsync(SharingViewModel))"
 397                       Disabled="@(ViewModel.IsSaving || !ViewModel.CanManageWorldDetails)"
 398                       StartIcon="@Icons.Material.Filled.Save">
 399                Save
 400            </MudButton>
 401        </div>
 402    </MudPaper>
 403}
 404
 405@code {
 406    [Parameter]
 407    public Guid WorldId { get; set; }
 408
 409    protected override async Task OnParametersSetAsync()
 410    {
 411        ViewModel.PropertyChanged += OnViewModelChanged;
 412        LinksViewModel.PropertyChanged += OnViewModelChanged;
 413        DocumentsViewModel.PropertyChanged += OnViewModelChanged;
 414        SharingViewModel.PropertyChanged += OnViewModelChanged;
 415
 416        await ViewModel.LoadAsync(WorldId, SharingViewModel, LinksViewModel, DocumentsViewModel);
 417        if (ViewModel.World != null && AppContext.CurrentWorldId != ViewModel.World.Id)
 418        {
 419            await AppContext.SelectWorldAsync(ViewModel.World.Id);
 420        }
 421    }
 422
 423    private async Task SaveWorldSlug()
 424    {
 425        var newSlug = await SharingViewModel.SaveSlugAsync(WorldId);
 426        if (newSlug != null && ViewModel.World != null)
 427            ViewModel.World.Slug = newSlug;
 428    }
 429
 430    private async Task CopyPublicUrl()
 431    {
 432        var url = SharingViewModel.GetFullPublicUrl(Navigation.BaseUri, ViewModel.World);
 433        if (string.IsNullOrEmpty(url)) return;
 434        try
 435        {
 436            await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", url);
 437        }
 438        catch { /* clipboard not available in all contexts */ }
 439    }
 440
 441    private void OnViewModelChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
 9442        => InvokeAsync(StateHasChanged);
 443
 444    public void Dispose()
 445    {
 3446        ViewModel.PropertyChanged -= OnViewModelChanged;
 3447        LinksViewModel.PropertyChanged -= OnViewModelChanged;
 3448        DocumentsViewModel.PropertyChanged -= OnViewModelChanged;
 3449        SharingViewModel.PropertyChanged -= OnViewModelChanged;
 3450    }
 451}