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