< Summary

Information
Class: Chronicis.Client.Components.Dashboard.WorldPanel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Dashboard/WorldPanel.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 52
Coverable lines: 52
Total lines: 312
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 50
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()100%210%
get_World()100%210%
get_IsExpanded()100%210%
get_IsExpandedChanged()100%210%
get_Class()100%210%
ToggleExpanded()100%210%
ViewWorld()100%210%
AddSessionNote()0%620%
NavigateToCharacter()0%2040%
FormatRelativeTime(...)0%110100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Dashboard/WorldPanel.razor

#LineLine coverage
 1@using Chronicis.Client.Components.Shared
 2@inject NavigationManager Navigation
 3@inject IArticleApiService ArticleApi
 4
 5<MudPaper Elevation="2" Class="@($"world-panel {(IsExpanded ? "expanded" : "collapsed")} {Class}")">
 6    <!-- World Header -->
 7    <div class="world-header" @onclick="ToggleExpanded">
 8        <div class="d-flex align-center">
 9            <MudIcon Icon="@Icons.Material.Filled.Public"
 10                     Size="Size.Large"
 11                     Style="color: var(--chronicis-beige-gold); margin-right: 16px;" />
 12            <div class="flex-grow-1">
 013                <MudText Typo="Typo.h4" Class="world-name">@World.Name</MudText>
 014                @if (!string.IsNullOrEmpty(World.Description))
 15                {
 016                    <MudText Typo="Typo.body2" Class="world-description">@World.Description</MudText>
 17                }
 18            </div>
 19            <MudIcon Icon="@(IsExpanded ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
 20                     Class="expand-icon" />
 21        </div>
 22    </div>
 23
 024    @if (IsExpanded)
 25    {
 26        <div class="world-content">
 27            <!-- Stats Row -->
 28            <div class="stats-row">
 29                <div class="stat-item">
 30                    <MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Small" />
 031                    <span>@World.ArticleCount articles</span>
 32                </div>
 33                <div class="stat-item">
 34                    <MudIcon Icon="@Icons.Material.Filled.Folder" Size="Size.Small" />
 035                    <span>@World.Campaigns.Count campaign@(World.Campaigns.Count != 1 ? "s" : "")</span>
 36                </div>
 37                <div class="stat-item">
 38                    <MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
 039                    <span>@World.MyCharacters.Count character@(World.MyCharacters.Count != 1 ? "s" : "")</span>
 40                </div>
 41            </div>
 42
 43            <!-- Active Campaign Section -->
 044            @if (World.Campaigns.Any(c => c.IsActive))
 45            {
 046                var activeCampaign = World.Campaigns.First(c => c.IsActive);
 47                <div class="section">
 48                    <MudText Typo="Typo.overline" Class="section-title">
 49                        <MudIcon Icon="@Icons.Material.Filled.PlayCircle" Size="Size.Small" Class="mr-1" />
 50                        Active Campaign
 51                    </MudText>
 52                    <MudPaper Elevation="0" Class="campaign-card">
 053                        <MudText Typo="Typo.body1" Class="campaign-name">@activeCampaign.Name</MudText>
 054                        @if (activeCampaign.CurrentArc != null)
 55                        {
 56                            <MudText Typo="Typo.caption" Class="arc-info">
 057                                Current Arc: <strong>@activeCampaign.CurrentArc.Name</strong>
 058                                (@activeCampaign.CurrentArc.SessionCount session@(activeCampaign.CurrentArc.SessionCount
 59                            </MudText>
 060                            @if (activeCampaign.CurrentArc.LatestSessionDate.HasValue)
 61                            {
 62                                <MudText Typo="Typo.caption" Class="last-session">
 063                                    Last session: @FormatRelativeTime(activeCampaign.CurrentArc.LatestSessionDate.Value)
 64                                </MudText>
 65                            }
 66                        }
 67                        <div class="campaign-stats">
 68                            <MudChip T="string" Size="Size.Small" Variant="Variant.Text">
 069                                @activeCampaign.SessionCount sessions
 70                            </MudChip>
 71                            <MudChip T="string" Size="Size.Small" Variant="Variant.Text">
 072                                @activeCampaign.ArcCount arcs
 73                            </MudChip>
 74                        </div>
 75                    </MudPaper>
 76                </div>
 77            }
 078            else if (!World.Campaigns.Any())
 79            {
 80                <div class="section empty-section">
 81                    <MudText Typo="Typo.body2" Color="Color.Secondary">
 82                        No campaigns yet. Create your first campaign to start tracking your adventures!
 83                    </MudText>
 84                </div>
 85            }
 86
 87            <!-- My Characters Section -->
 088            @if (World.MyCharacters.Any())
 89            {
 90                <div class="section">
 91                    <MudText Typo="Typo.overline" Class="section-title">
 92                        <MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" Class="mr-1" />
 93                        My Characters
 94                    </MudText>
 95                    <div class="characters-list">
 096                        @foreach (var character in World.MyCharacters)
 97                        {
 098                            <div class="character-chip" @onclick="async () => await NavigateToCharacter(character.Id)" @
 99                                <IconDisplay Icon="@character.IconEmoji" DefaultIcon="👤" CssClass="character-chip-icon"
 0100                                <span>@character.Title</span>
 101                            </div>
 102                        }
 103                    </div>
 104                </div>
 105            }
 106        </div>
 107    }
 108</MudPaper>
 109
 110<style>
 111    .world-panel {
 112        border-radius: var(--chronicis-radius-lg);
 113        overflow: hidden;
 114        transition: all var(--chronicis-transition-normal);
 115    }
 116
 117    .world-header {
 118        padding: var(--chronicis-space-lg);
 119        cursor: pointer;
 120        background: linear-gradient(135deg, rgba(31, 42, 51, 0.03) 0%, rgba(196, 175, 142, 0.05) 100%);
 121        border-bottom: 1px solid rgba(196, 175, 142, 0.1);
 122    }
 123
 124    .world-header:hover {
 125        background: linear-gradient(135deg, rgba(31, 42, 51, 0.05) 0%, rgba(196, 175, 142, 0.08) 100%);
 126    }
 127
 128    .world-name {
 129        font-family: var(--chronicis-font-heading);
 130        color: var(--chronicis-charcoal);
 131        margin: 0;
 132        font-size: 1.75rem;
 133    }
 134
 135    .world-description {
 136        color: var(--chronicis-slate-grey);
 137        margin-top: var(--chronicis-space-xs);
 138        opacity: 0.8;
 139    }
 140
 141    .expand-icon {
 142        color: var(--chronicis-slate-grey);
 143        transition: transform var(--chronicis-transition-fast);
 144    }
 145
 146    .world-content {
 147        padding: var(--chronicis-space-lg);
 148    }
 149
 150    .stats-row {
 151        display: flex;
 152        gap: var(--chronicis-space-lg);
 153        margin-bottom: var(--chronicis-space-lg);
 154        padding-bottom: var(--chronicis-space-md);
 155        border-bottom: 1px solid rgba(196, 175, 142, 0.1);
 156    }
 157
 158    .stat-item {
 159        display: flex;
 160        align-items: center;
 161        gap: var(--chronicis-space-xs);
 162        color: var(--chronicis-slate-grey);
 163        font-size: 0.875rem;
 164    }
 165
 166    .section {
 167        margin-bottom: var(--chronicis-space-lg);
 168    }
 169
 170    .section:last-child {
 171        margin-bottom: 0;
 172    }
 173
 174    .section-title {
 175        display: flex;
 176        align-items: center;
 177        color: var(--chronicis-slate-grey);
 178        margin-bottom: var(--chronicis-space-sm);
 179        font-weight: 500;
 180        letter-spacing: 0.5px;
 181        text-transform: uppercase;
 182        font-size: 0.7rem;
 183    }
 184
 185    .campaign-card {
 186        background: rgba(196, 175, 142, 0.05);
 187        border: 1px solid rgba(196, 175, 142, 0.15);
 188        border-radius: var(--chronicis-radius-md);
 189        padding: var(--chronicis-space-md);
 190    }
 191
 192    .campaign-name {
 193        font-weight: 500;
 194        color: var(--chronicis-charcoal);
 195    }
 196
 197    .arc-info, .last-session {
 198        color: var(--chronicis-slate-grey);
 199        display: block;
 200        margin-top: var(--chronicis-space-xs);
 201    }
 202
 203    .campaign-stats {
 204        margin-top: var(--chronicis-space-sm);
 205        display: flex;
 206        gap: var(--chronicis-space-sm);
 207    }
 208
 209    .characters-list {
 210        display: flex;
 211        flex-wrap: wrap;
 212        gap: var(--chronicis-space-sm);
 213    }
 214
 215    .character-chip {
 216        display: inline-flex;
 217        align-items: center;
 218        gap: 6px;
 219        padding: 6px 12px;
 220        border-radius: 16px;
 221        border: 1px solid var(--chronicis-beige-gold);
 222        color: var(--chronicis-charcoal);
 223        font-size: 0.875rem;
 224        cursor: pointer;
 225        transition: all var(--chronicis-transition-fast);
 226    }
 227
 228    .character-chip:hover {
 229        background: rgba(196, 175, 142, 0.15);
 230        border-color: var(--chronicis-beige-gold);
 231    }
 232
 233    .character-chip-icon {
 234        font-size: 1rem;
 235    }
 236
 237    .actions-section {
 238        display: flex;
 239        gap: var(--chronicis-space-sm);
 240        flex-wrap: wrap;
 241    }
 242
 243    .empty-section {
 244        padding: var(--chronicis-space-md);
 245        background: rgba(196, 175, 142, 0.03);
 246        border-radius: var(--chronicis-radius-md);
 247        text-align: center;
 248    }
 249</style>
 250
 251@code {
 252    [Parameter, EditorRequired]
 0253    public DashboardWorldDto World { get; set; } = null!;
 254
 255    [Parameter]
 0256    public bool IsExpanded { get; set; } = true;
 257
 258    [Parameter]
 0259    public EventCallback<bool> IsExpandedChanged { get; set; }
 260
 261    [Parameter]
 0262    public string? Class { get; set; }
 263
 264    private void ToggleExpanded()
 265    {
 0266        IsExpanded = !IsExpanded;
 0267        IsExpandedChanged.InvokeAsync(IsExpanded);
 0268    }
 269
 270    private void ViewWorld()
 271    {
 272        // Always navigate to world page - it handles missing root article
 0273        Navigation.NavigateTo($"/world/{World.Slug}", forceLoad: true);
 0274    }
 275
 276    private void AddSessionNote()
 277    {
 0278        var activeCampaign = World.Campaigns.FirstOrDefault(c => c.IsActive);
 0279        if (activeCampaign != null)
 280        {
 0281            Navigation.NavigateTo($"/world/{World.Slug}/campaign/{activeCampaign.Id}", forceLoad: true);
 282        }
 0283    }
 284
 285    private async Task NavigateToCharacter(Guid characterId)
 286    {
 0287        var article = await ArticleApi.GetArticleDetailAsync(characterId);
 0288        if (article != null && article.Breadcrumbs.Any())
 289        {
 0290            var path = string.Join("/", article.Breadcrumbs.Select(b => b.Slug));
 0291            Navigation.NavigateTo($"/article/{path}");
 292        }
 0293    }
 294
 295    private string FormatRelativeTime(DateTime dateTime)
 296    {
 0297        var timeSpan = DateTime.UtcNow - dateTime;
 298
 0299        if (timeSpan.TotalMinutes < 1)
 0300            return "just now";
 0301        if (timeSpan.TotalMinutes < 60)
 0302            return $"{(int)timeSpan.TotalMinutes}m ago";
 0303        if (timeSpan.TotalHours < 24)
 0304            return $"{(int)timeSpan.TotalHours}h ago";
 0305        if (timeSpan.TotalDays < 7)
 0306            return $"{(int)timeSpan.TotalDays}d ago";
 0307        if (timeSpan.TotalDays < 30)
 0308            return $"{(int)(timeSpan.TotalDays / 7)}w ago";
 309
 0310        return dateTime.ToString("MMM d, yyyy");
 311    }
 312}