< 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
100%
Covered lines: 15
Uncovered lines: 0
Coverable lines: 15
Total lines: 307
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ToggleExpanded()100%11100%
FormatRelativeTime(...)100%1010100%

File(s)

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

#LineLine coverage
 1@using Chronicis.Client.Components.Shared
 2@using Chronicis.Client.Abstractions
 3@inject IArticleApiService ArticleApi
 4@inject IAppNavigator AppNavigator
 5
 6<MudPaper Elevation="2" Class="@($"world-panel {(IsExpanded ? "expanded" : "collapsed")} {Class}")">
 7    <!-- World Header -->
 8    <div class="world-header" @onclick="ToggleExpanded">
 9        <div class="d-flex align-center">
 10            <MudIcon Icon="@Icons.Material.Filled.Public"
 11                     Size="Size.Large"
 12                     Style="color: var(--chronicis-beige-gold); margin-right: 16px;" />
 13            <div class="flex-grow-1">
 14                <MudText Typo="Typo.h4" Class="world-name">@World.Name</MudText>
 15                @if (!string.IsNullOrEmpty(World.Description))
 16                {
 17                    <MudText Typo="Typo.body2" Class="world-description">@World.Description</MudText>
 18                }
 19            </div>
 20            <MudIcon Icon="@(IsExpanded ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
 21                     Class="expand-icon" />
 22        </div>
 23    </div>
 24
 25    @if (IsExpanded)
 26    {
 27        <div class="world-content">
 28            <!-- Stats Row -->
 29            <div class="stats-row">
 30                <div class="stat-item">
 31                    <MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Small" />
 32                    <span>@World.ArticleCount articles</span>
 33                </div>
 34                <div class="stat-item">
 35                    <MudIcon Icon="@Icons.Material.Filled.Folder" Size="Size.Small" />
 36                    <span>@World.Campaigns.Count campaign@(World.Campaigns.Count != 1 ? "s" : "")</span>
 37                </div>
 38                <div class="stat-item">
 39                    <MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
 40                    <span>@World.MyCharacters.Count character@(World.MyCharacters.Count != 1 ? "s" : "")</span>
 41                </div>
 42            </div>
 43
 44            <!-- Active Campaign Section -->
 45            @if (World.Campaigns.Any(c => c.IsActive))
 46            {
 47                var activeCampaign = World.Campaigns.First(c => c.IsActive);
 48                <div class="section">
 49                    <MudText Typo="Typo.overline" Class="section-title">
 50                        <MudIcon Icon="@Icons.Material.Filled.PlayCircle" Size="Size.Small" Class="mr-1" />
 51                        Active Campaign
 52                    </MudText>
 53                    <MudPaper Elevation="0" Class="campaign-card">
 54                        <MudText Typo="Typo.body1" Class="campaign-name">@activeCampaign.Name</MudText>
 55                        @if (activeCampaign.CurrentArc != null)
 56                        {
 57                            <MudText Typo="Typo.caption" Class="arc-info">
 58                                Current Arc: <strong>@activeCampaign.CurrentArc.Name</strong>
 59                                (@activeCampaign.CurrentArc.SessionCount session@(activeCampaign.CurrentArc.SessionCount
 60                            </MudText>
 61                            @if (activeCampaign.CurrentArc.LatestSessionDate.HasValue)
 62                            {
 63                                <MudText Typo="Typo.caption" Class="last-session">
 64                                    Last session: @FormatRelativeTime(activeCampaign.CurrentArc.LatestSessionDate.Value)
 65                                </MudText>
 66                            }
 67                        }
 68                        <div class="campaign-stats">
 69                            <MudChip T="string" Size="Size.Small" Variant="Variant.Text">
 70                                @activeCampaign.SessionCount sessions
 71                            </MudChip>
 72                            <MudChip T="string" Size="Size.Small" Variant="Variant.Text">
 73                                @activeCampaign.ArcCount arcs
 74                            </MudChip>
 75                        </div>
 76                    </MudPaper>
 77                </div>
 78            }
 79            else if (!World.Campaigns.Any())
 80            {
 81                <div class="section empty-section">
 82                    <MudText Typo="Typo.body2" Color="Color.Secondary">
 83                        No campaigns yet. Create your first campaign to start tracking your adventures!
 84                    </MudText>
 85                </div>
 86            }
 87
 88            <!-- My Characters Section -->
 89            @if (World.MyCharacters.Any())
 90            {
 91                <div class="section">
 92                    <MudText Typo="Typo.overline" Class="section-title">
 93                        <MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" Class="mr-1" />
 94                        My Characters
 95                    </MudText>
 96                    <div class="characters-list">
 97                        @foreach (var character in World.MyCharacters)
 98                        {
 99                            <div class="character-chip" @onclick="async () => await NavigateToCharacter(character.Id)" @
 100                                <IconDisplay Icon="@character.IconEmoji" DefaultIcon="👤" CssClass="character-chip-icon"
 101                                <span>@character.Title</span>
 102                            </div>
 103                        }
 104                    </div>
 105                </div>
 106            }
 107        </div>
 108    }
 109</MudPaper>
 110
 111<style>
 112    .world-panel {
 113        border-radius: var(--chronicis-radius-lg);
 114        overflow: hidden;
 115        transition: all var(--chronicis-transition-normal);
 116    }
 117
 118    .world-header {
 119        padding: var(--chronicis-space-lg);
 120        cursor: pointer;
 121        background: linear-gradient(135deg, rgba(31, 42, 51, 0.03) 0%, rgba(196, 175, 142, 0.05) 100%);
 122        border-bottom: 1px solid rgba(196, 175, 142, 0.1);
 123    }
 124
 125    .world-header:hover {
 126        background: linear-gradient(135deg, rgba(31, 42, 51, 0.05) 0%, rgba(196, 175, 142, 0.08) 100%);
 127    }
 128
 129    .world-name {
 130        font-family: var(--chronicis-font-heading);
 131        color: var(--chronicis-charcoal);
 132        margin: 0;
 133        font-size: 1.75rem;
 134    }
 135
 136    .world-description {
 137        color: var(--chronicis-slate-grey);
 138        margin-top: var(--chronicis-space-xs);
 139        opacity: 0.8;
 140    }
 141
 142    .expand-icon {
 143        color: var(--chronicis-slate-grey);
 144        transition: transform var(--chronicis-transition-fast);
 145    }
 146
 147    .world-content {
 148        padding: var(--chronicis-space-lg);
 149    }
 150
 151    .stats-row {
 152        display: flex;
 153        gap: var(--chronicis-space-lg);
 154        margin-bottom: var(--chronicis-space-lg);
 155        padding-bottom: var(--chronicis-space-md);
 156        border-bottom: 1px solid rgba(196, 175, 142, 0.1);
 157    }
 158
 159    .stat-item {
 160        display: flex;
 161        align-items: center;
 162        gap: var(--chronicis-space-xs);
 163        color: var(--chronicis-slate-grey);
 164        font-size: 0.875rem;
 165    }
 166
 167    .section {
 168        margin-bottom: var(--chronicis-space-lg);
 169    }
 170
 171    .section:last-child {
 172        margin-bottom: 0;
 173    }
 174
 175    .section-title {
 176        display: flex;
 177        align-items: center;
 178        color: var(--chronicis-slate-grey);
 179        margin-bottom: var(--chronicis-space-sm);
 180        font-weight: 500;
 181        letter-spacing: 0.5px;
 182        text-transform: uppercase;
 183        font-size: 0.7rem;
 184    }
 185
 186    .campaign-card {
 187        background: rgba(196, 175, 142, 0.05);
 188        border: 1px solid rgba(196, 175, 142, 0.15);
 189        border-radius: var(--chronicis-radius-md);
 190        padding: var(--chronicis-space-md);
 191    }
 192
 193    .campaign-name {
 194        font-weight: 500;
 195        color: var(--chronicis-charcoal);
 196    }
 197
 198    .arc-info, .last-session {
 199        color: var(--chronicis-slate-grey);
 200        display: block;
 201        margin-top: var(--chronicis-space-xs);
 202    }
 203
 204    .campaign-stats {
 205        margin-top: var(--chronicis-space-sm);
 206        display: flex;
 207        gap: var(--chronicis-space-sm);
 208    }
 209
 210    .characters-list {
 211        display: flex;
 212        flex-wrap: wrap;
 213        gap: var(--chronicis-space-sm);
 214    }
 215
 216    .character-chip {
 217        display: inline-flex;
 218        align-items: center;
 219        gap: 6px;
 220        padding: 6px 12px;
 221        border-radius: 16px;
 222        border: 1px solid var(--chronicis-beige-gold);
 223        color: var(--chronicis-charcoal);
 224        font-size: 0.875rem;
 225        cursor: pointer;
 226        transition: all var(--chronicis-transition-fast);
 227    }
 228
 229    .character-chip:hover {
 230        background: rgba(196, 175, 142, 0.15);
 231        border-color: var(--chronicis-beige-gold);
 232    }
 233
 234    .character-chip-icon {
 235        font-size: 1rem;
 236    }
 237
 238    .actions-section {
 239        display: flex;
 240        gap: var(--chronicis-space-sm);
 241        flex-wrap: wrap;
 242    }
 243
 244    .empty-section {
 245        padding: var(--chronicis-space-md);
 246        background: rgba(196, 175, 142, 0.03);
 247        border-radius: var(--chronicis-radius-md);
 248        text-align: center;
 249    }
 250</style>
 251
 252@code {
 253    [Parameter, EditorRequired]
 254    public DashboardWorldDto World { get; set; } = null!;
 255
 256    [Parameter]
 257    public bool IsExpanded { get; set; } = true;
 258
 259    [Parameter]
 260    public EventCallback<bool> IsExpandedChanged { get; set; }
 261
 262    [Parameter]
 263    public string? Class { get; set; }
 264
 265    private void ToggleExpanded()
 266    {
 1267        IsExpanded = !IsExpanded;
 1268        IsExpandedChanged.InvokeAsync(IsExpanded);
 1269    }
 270
 271    private async Task ViewWorld()
 272    {
 273        await AppNavigator.GoToWorldAsync(World.Slug);
 274    }
 275
 276    private async Task AddSessionNote()
 277    {
 278        var activeCampaign = World.Campaigns.FirstOrDefault(c => c.IsActive);
 279        if (activeCampaign != null)
 280            await AppNavigator.GoToCampaignAsync(World.Slug, activeCampaign.Slug);
 281    }
 282
 283    private async Task NavigateToCharacter(Guid characterId)
 284    {
 285        var article = await ArticleApi.GetArticleDetailAsync(characterId);
 286        if (article != null)
 287            await AppNavigator.GoToArticleAsync(article);
 288    }
 289
 290    private string FormatRelativeTime(DateTime dateTime)
 291    {
 7292        var timeSpan = DateTime.UtcNow - dateTime;
 293
 7294        if (timeSpan.TotalMinutes < 1)
 1295            return "just now";
 6296        if (timeSpan.TotalMinutes < 60)
 1297            return $"{(int)timeSpan.TotalMinutes}m ago";
 5298        if (timeSpan.TotalHours < 24)
 1299            return $"{(int)timeSpan.TotalHours}h ago";
 4300        if (timeSpan.TotalDays < 7)
 2301            return $"{(int)timeSpan.TotalDays}d ago";
 2302        if (timeSpan.TotalDays < 30)
 1303            return $"{(int)(timeSpan.TotalDays / 7)}w ago";
 304
 1305        return dateTime.ToString("MMM d, yyyy");
 306    }
 307}