< Summary

Information
Class: Chronicis.Client.Pages.PublicWorldPage
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/PublicWorldPage.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 113
Coverable lines: 113
Total lines: 393
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 106
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%272160%
get_PublicSlug()100%210%
get_ArticlePath()100%210%
.ctor()100%210%
OnParametersSetAsync()100%210%
OnAfterRenderAsync()0%7280%
Dispose()0%620%
OnPublicWikiLinkClicked()0%2040%
LoadWorldAsync()0%2040%
LoadArticleAsync()0%620%
NavigateToArticle(...)0%620%
GetBreadcrumbItems()0%1332360%
GetArticleTypeLabel(...)0%156120%

File(s)

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

#LineLine coverage
 1@page "/w/{PublicSlug}"
 2@page "/w/{PublicSlug}/{*ArticlePath}"
 3@layout PublicLayout
 4@using Chronicis.Shared.DTOs
 5@using Chronicis.Shared.Enums
 6@using Chronicis.Client.Components.Shared
 7@using Microsoft.JSInterop
 8@inject IPublicApiService PublicApi
 9@inject IMarkdownService MarkdownService
 10@inject NavigationManager Navigation
 11@inject IJSRuntime JSRuntime
 12
 13@implements IDisposable
 14
 015<PageTitle>@(_world?.Name ?? "World") — Chronicis</PageTitle>
 16
 017@if (_isLoading)
 18{
 19    <div class="public-world-loading">
 20        <MudProgressCircular Color="Color.Primary" Indeterminate="true" />
 21        <MudText Typo="Typo.body1" Class="mt-3">Loading world...</MudText>
 22    </div>
 23}
 024else if (_world == null)
 25{
 26    <div class="public-world-not-found">
 27        <MudPaper Elevation="2" Class="pa-6 text-center">
 28            <MudIcon Icon="@Icons.Material.Filled.SearchOff"
 29                     Style="font-size: 64px; color: var(--chronicis-beige-gold);" />
 30            <MudText Typo="Typo.h5" Class="mt-4 mb-2">World Not Found</MudText>
 31            <MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
 32                This world doesn't exist or isn't publicly shared.
 33            </MudText>
 34            <MudButton Variant="Variant.Filled"
 35                       Color="Color.Primary"
 36                       Href="/"
 37                       StartIcon="@Icons.Material.Filled.Home">
 38                Go Home
 39            </MudButton>
 40        </MudPaper>
 41    </div>
 42}
 43else
 44{
 45    <div class="public-world-container">
 46        <!-- Sidebar with article tree -->
 47        <aside class="public-world-sidebar">
 48            <div class="public-world-sidebar-header">
 49                <a href="/w/@PublicSlug" class="public-world-title-link">
 50                    <MudText Typo="Typo.subtitle1" Style="color: var(--chronicis-beige-gold); font-weight: 600;">
 051                        @_world.Name
 52                    </MudText>
 53                </a>
 54            </div>
 55
 56            <MudDivider Class="my-2" Style="opacity: 0.3;" />
 57
 058            @if (_articleTree.Count == 0)
 59            {
 60                <div class="pa-3">
 61                    <MudText Typo="Typo.body2" Class="mud-text-secondary">
 62                        No public articles in this world yet.
 63                    </MudText>
 64                </div>
 65            }
 66            else
 67            {
 68                <div class="public-article-tree">
 069                    @foreach (var article in _articleTree)
 70                    {
 71                        <PublicArticleTreeItem Article="@article"
 72                                               PublicSlug="@PublicSlug"
 73                                               CurrentPath="@ArticlePath"
 74                                               OnArticleSelected="NavigateToArticle" />
 75                    }
 76                </div>
 77            }
 78        </aside>
 79
 80        <!-- Main content area -->
 81        <main class="public-world-main">
 082            @if (string.IsNullOrEmpty(ArticlePath))
 83            {
 84                <!-- World landing view -->
 85                <MudPaper Elevation="2" Class="pa-6">
 86                    <div class="public-world-hero">
 87                        <MudIcon Icon="@Icons.Material.Filled.Public"
 88                                 Style="font-size: 48px; color: var(--chronicis-beige-gold);" />
 089                        <MudText Typo="Typo.h4" Class="mt-3">@_world.Name</MudText>
 090                        @if (!string.IsNullOrEmpty(_world.Description))
 91                        {
 92                            <MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
 093                                @_world.Description
 94                            </MudText>
 95                        }
 96                        <MudText Typo="Typo.caption" Class="mud-text-secondary mt-3">
 097                            Created by @_world.OwnerName
 98                        </MudText>
 99                    </div>
 100
 0101                    @if (_articleTree.Count > 0)
 102                    {
 103                        <MudDivider Class="my-4" />
 104                        <MudText Typo="Typo.body2" Class="mud-text-secondary">
 105                            Select an article from the sidebar to start reading.
 106                        </MudText>
 107                    }
 108                </MudPaper>
 109            }
 0110            else if (_currentArticle == null && !_isLoadingArticle)
 111            {
 112                <!-- Article not found -->
 113                <MudPaper Elevation="2" Class="pa-6 text-center">
 114                    <MudIcon Icon="@Icons.Material.Filled.Article"
 115                             Style="font-size: 48px; color: var(--chronicis-beige-gold);" />
 116                    <MudText Typo="Typo.h6" Class="mt-3">Article Not Found</MudText>
 117                    <MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
 118                        This article doesn't exist or isn't publicly visible.
 119                    </MudText>
 120                    <MudButton Variant="Variant.Text"
 121                               Color="Color.Primary"
 122                               Class="mt-3"
 0123                               OnClick="() => NavigateToArticle(string.Empty)">
 124                        Back to World Overview
 125                    </MudButton>
 126                </MudPaper>
 127            }
 0128            else if (_isLoadingArticle)
 129            {
 130                <MudPaper Elevation="2" Class="pa-6">
 131                    <MudSkeleton Width="60%" Height="32px" />
 132                    <MudSkeleton Width="100%" Height="20px" Class="mt-4" />
 133                    <MudSkeleton Width="100%" Height="20px" Class="mt-2" />
 134                    <MudSkeleton Width="80%" Height="20px" Class="mt-2" />
 135                </MudPaper>
 136            }
 137            else
 138            {
 139                <!-- Article view -->
 140                <MudPaper Elevation="2" Class="pa-6">
 141                    <!-- Breadcrumbs -->
 0142                    @if (_currentArticle!.Breadcrumbs?.Any() == true)
 143                    {
 144                        <ChroniclsBreadcrumbs Items="@GetBreadcrumbItems()" UseCustomLinks="true" Class="mb-3 pa-0" />
 145                    }
 146
 147                    <!-- Article header -->
 148                    <div class="d-flex align-center gap-3 mb-4">
 0149                        @if (!string.IsNullOrEmpty(_currentArticle.IconEmoji))
 150                        {
 151                            <IconDisplay Icon="@_currentArticle.IconEmoji" DefaultIcon="" Style="font-size: 2rem;" />
 152                        }
 153                        <div>
 0154                            <MudText Typo="Typo.h4">@_currentArticle.Title</MudText>
 155                            <MudText Typo="Typo.caption" Class="mud-text-secondary">
 0156                                @GetArticleTypeLabel(_currentArticle.Type)
 0157                                @if (_currentArticle.ModifiedAt.HasValue)
 158                                {
 0159                                    <text> · Updated @_currentArticle.ModifiedAt.Value.ToString("MMM d, yyyy")</text>
 160                                }
 161                            </MudText>
 162                        </div>
 163                    </div>
 164
 165                    <!-- AI Summary (if available) -->
 0166                    @if (!string.IsNullOrEmpty(_currentArticle.AISummary))
 167                    {
 168                        <MudAlert Severity="Severity.Info" Class="mb-4 summary-text" Dense="true">
 0169                            <strong>AI Summary:</strong> @_currentArticle.AISummary
 170                        </MudAlert>
 171                    }
 172
 173                    <!-- Article body -->
 174                    <div id="public-article-body" class="public-article-body chronicis-editor-content">
 0175                        @if (!string.IsNullOrEmpty(_currentArticle.Body))
 176                        {
 0177                            @((MarkupString)MarkdownService.EnsureHtml(_currentArticle.Body))
 178                        }
 179                        else
 180                        {
 181                            <MudText Typo="Typo.body2" Class="mud-text-secondary">
 182                                <em>No content available.</em>
 183                            </MudText>
 184                        }
 185                    </div>
 186                </MudPaper>
 187            }
 188        </main>
 189    </div>
 190}
 191
 192@code {
 193    [Parameter]
 0194    public string PublicSlug { get; set; } = string.Empty;
 195
 196    [Parameter]
 0197    public string? ArticlePath { get; set; }
 198
 199    private WorldDetailDto? _world;
 0200    private List<ArticleTreeDto> _articleTree = new();
 201    private ArticleDto? _currentArticle;
 0202    private bool _isLoading = true;
 203    private bool _isLoadingArticle = false;
 204    private DotNetObjectReference<PublicWorldPage>? _dotNetHelper;
 205    private bool _wikiLinksInitialized = false;
 206
 207    protected override async Task OnParametersSetAsync()
 208    {
 209        // Reset wiki links state when navigating between articles
 0210        _wikiLinksInitialized = false;
 0211        await LoadWorldAsync();
 0212    }
 213
 214    protected override async Task OnAfterRenderAsync(bool firstRender)
 215    {
 216        // Initialize wiki link click handlers after the article body is rendered
 0217        if (_currentArticle != null && !string.IsNullOrEmpty(_currentArticle.Body) && !_wikiLinksInitialized)
 218        {
 0219            _dotNetHelper ??= DotNetObjectReference.Create(this);
 220            try
 221            {
 0222                await JSRuntime.InvokeVoidAsync("initializePublicWikiLinks", "public-article-body", _dotNetHelper);
 0223                _wikiLinksInitialized = true;
 0224            }
 0225            catch (Exception)
 226            {
 227                // JS interop may fail during prerendering or if component is disposed
 0228            }
 229        }
 0230    }
 231
 232    public void Dispose()
 233    {
 0234        _dotNetHelper?.Dispose();
 0235    }
 236
 237    [JSInvokable]
 238    public async Task OnPublicWikiLinkClicked(string targetArticleId)
 239    {
 0240        if (!Guid.TryParse(targetArticleId, out var articleId)) return;
 241
 242        try
 243        {
 0244            var path = await PublicApi.ResolvePublicArticlePathAsync(PublicSlug, articleId);
 0245            if (!string.IsNullOrEmpty(path))
 246            {
 0247                Navigation.NavigateTo($"/w/{PublicSlug}/{path}");
 248            }
 0249        }
 0250        catch (Exception)
 251        {
 252            // Silently fail - link may not be public or may not exist
 0253        }
 0254    }
 255
 256    private async Task LoadWorldAsync()
 257    {
 0258        _isLoading = true;
 0259        StateHasChanged();
 260
 261        try
 262        {
 263            // Load world info
 0264            _world = await PublicApi.GetPublicWorldAsync(PublicSlug);
 265
 0266            if (_world != null)
 267            {
 268                // Load article tree
 0269                _articleTree = await PublicApi.GetPublicArticleTreeAsync(PublicSlug);
 270
 271                // Load specific article if path provided
 0272                if (!string.IsNullOrEmpty(ArticlePath))
 273                {
 0274                    await LoadArticleAsync();
 275                }
 276            }
 0277        }
 278        finally
 279        {
 0280            _isLoading = false;
 0281            StateHasChanged();
 282        }
 0283    }
 284
 285    private async Task LoadArticleAsync()
 286    {
 0287        if (string.IsNullOrEmpty(ArticlePath)) return;
 288
 0289        _isLoadingArticle = true;
 0290        StateHasChanged();
 291
 292        try
 293        {
 0294            _currentArticle = await PublicApi.GetPublicArticleAsync(PublicSlug, ArticlePath);
 0295        }
 296        finally
 297        {
 0298            _isLoadingArticle = false;
 0299            StateHasChanged();
 300        }
 0301    }
 302
 303    private void NavigateToArticle(string articlePath)
 304    {
 0305        if (string.IsNullOrEmpty(articlePath))
 306        {
 0307            Navigation.NavigateTo($"/w/{PublicSlug}");
 308        }
 309        else
 310        {
 0311            Navigation.NavigateTo($"/w/{PublicSlug}/{articlePath}");
 312        }
 0313    }
 314
 315    private List<BreadcrumbItem> GetBreadcrumbItems()
 316    {
 0317        var items = new List<BreadcrumbItem>();
 318
 0319        if (_currentArticle?.Breadcrumbs == null) return items;
 320
 321        // Determine which breadcrumbs are virtual (Campaign/Arc/Player Characters/Wiki) vs real articles
 0322        var campaignId = _currentArticle.CampaignId;
 0323        var arcId = _currentArticle.ArcId;
 324
 325        // Virtual group slugs that shouldn't be clickable
 0326        var virtualGroupSlugs = new HashSet<string> { "characters", "wiki", "campaigns", "uncategorized" };
 327
 0328        foreach (var crumb in _currentArticle.Breadcrumbs)
 329        {
 0330            var isLast = crumb.Id == _currentArticle.Id;
 331
 0332            if (crumb.IsWorld)
 333            {
 334                // World - always clickable, goes back to world landing
 0335                items.Add(new BreadcrumbItem(crumb.Title, $"/w/{PublicSlug}"));
 336            }
 0337            else if (crumb.Id == Guid.Empty || virtualGroupSlugs.Contains(crumb.Slug.ToLowerInvariant()))
 338            {
 339                // Virtual group breadcrumb (Player Characters, Wiki, etc.) - not clickable
 0340                items.Add(new BreadcrumbItem(crumb.Title, null, disabled: true));
 341            }
 0342            else if (campaignId.HasValue && crumb.Id == campaignId.Value)
 343            {
 344                // Campaign breadcrumb - virtual, not clickable
 0345                items.Add(new BreadcrumbItem(crumb.Title, null, disabled: true));
 346            }
 0347            else if (arcId.HasValue && crumb.Id == arcId.Value)
 348            {
 349                // Arc breadcrumb - virtual, not clickable
 0350                items.Add(new BreadcrumbItem(crumb.Title, null, disabled: true));
 351            }
 352            else
 353            {
 354                // Real article breadcrumb
 0355                if (isLast)
 356                {
 0357                    items.Add(new BreadcrumbItem(crumb.Title, null, disabled: true));
 358                }
 359                else
 360                {
 361                    // Build path for this article
 362                    // Only include real article slugs (not virtual groups)
 0363                    var articleSlugs = _currentArticle.Breadcrumbs
 0364                        .Where(b => !b.IsWorld &&
 0365                               b.Id != Guid.Empty &&
 0366                               !virtualGroupSlugs.Contains(b.Slug.ToLowerInvariant()) &&
 0367                               !(campaignId.HasValue && b.Id == campaignId.Value) &&
 0368                               !(arcId.HasValue && b.Id == arcId.Value))
 0369                        .TakeWhile(b => b.Id != crumb.Id)
 0370                        .Select(b => b.Slug)
 0371                        .ToList();
 0372                    articleSlugs.Add(crumb.Slug);
 0373                    var path = string.Join("/", articleSlugs);
 374
 0375                    items.Add(new BreadcrumbItem(crumb.Title, $"/w/{PublicSlug}/{path}"));
 376                }
 377            }
 378        }
 379
 0380        return items;
 381    }
 382
 0383    private static string GetArticleTypeLabel(ArticleType type) => type switch
 0384    {
 0385        ArticleType.WikiArticle => "Wiki Article",
 0386        ArticleType.Character => "Character",
 0387        ArticleType.CharacterNote => "Character Note",
 0388        ArticleType.Session => "Session",
 0389        ArticleType.SessionNote => "Session Note",
 0390        ArticleType.Legacy => "Article",
 0391        _ => "Article"
 0392    };
 393}