< Summary

Information
Class: Chronicis.Client.Pages.Dashboard
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/Dashboard.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 116
Coverable lines: 116
Total lines: 390
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 52
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
BuildRenderTree(...)0%156120%
<BuildRenderTree()100%210%
OnInitializedAsync()0%2040%
LoadDashboard()0%620%
LoadQuote()100%210%
CreateNewWorld()0%2040%
JoinWorld()0%7280%
NavigateToCharacter()0%2040%
HandlePromptClick(...)0%620%
GetCategoryClass(...)0%2040%
OnTreeStateChanged()100%210%
Dispose()100%210%

File(s)

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

#LineLine coverage
 1@page "/dashboard"
 2@attribute [Authorize]
 3@using Chronicis.Client.Components.Shared
 4@using Chronicis.Client.Components.Dialogs
 5@inject IDashboardApiService DashboardApi
 6@inject IUserApiService UserApi
 7@inject IWorldApiService WorldApi
 8@inject IQuoteService QuoteService
 9@inject ITreeStateService TreeStateService
 10@inject IDialogService DialogService
 11@inject NavigationManager Navigation
 12@inject ISnackbar Snackbar
 13@inject IArticleApiService ArticleApi
 14@implements IDisposable
 15
 16<PageTitle>Dashboard - Chronicis</PageTitle>
 17
 18@code {
 19    private readonly ILogger<Dashboard> _logger;
 20
 21    // Direct constructor injection
 022    public Dashboard(ILogger<Dashboard> logger)
 23    {
 024        _logger = logger;
 025    }
 26}
 27
 028@if (_isLoading)
 29{
 30    <MudPaper Elevation="2" Class="dashboard-container">
 31        <div class="dashboard-loading">
 32            <MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
 33            <MudText Typo="Typo.body1" Class="mt-4">Loading your chronicle...</MudText>
 34        </div>
 35    </MudPaper>
 36}
 037else if (_dashboard == null)
 38{
 39    <MudPaper Elevation="2" Class="dashboard-container">
 40        <div class="dashboard-error">
 41            <MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" />
 42            <MudText Typo="Typo.h6" Class="mt-4">Unable to load dashboard</MudText>
 43            <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadDashboard" Class="mt-4">
 44                Try Again
 45            </MudButton>
 46        </div>
 47    </MudPaper>
 48}
 49else
 50{
 51    <div class="dashboard-wrapper">
 52        <!-- Hero Section -->
 53        <div class="hero-section">
 54            <div class="hero-content">
 55                <div class="hero-text">
 56                    <MudText Typo="Typo.h3" Class="hero-title">
 057                        Welcome back, <span class="user-name">@_dashboard.UserDisplayName</span>
 58                    </MudText>
 59                    <MudText Typo="Typo.body1" Class="hero-subtitle">
 60                        Your chronicle awaits. What story will you tell today?
 61                    </MudText>
 062                    @if (_quote != null)
 63                    {
 64                        <div class="hero-quote">
 65                            <MudText Typo="Typo.body2" Class="quote-text">
 066                                "@_quote.Content"
 67                            </MudText>
 68                            <MudText Typo="Typo.caption" Class="quote-author">
 069                                — @_quote.Author
 70                            </MudText>
 71                        </div>
 72                    }
 73                </div>
 74                <div class="hero-actions">
 75                    <MudButton Variant="Variant.Outlined"
 76                               Color="Color.Primary"
 77                               Size="Size.Large"
 78                               OnClick="JoinWorld"
 79                               Class="hero-button">
 80                        Join a World
 81                    </MudButton>
 82                </div>
 83            </div>
 84
 85            <!-- Prompts in Hero -->
 086            @if (_dashboard.Prompts.Any())
 87            {
 88                <div class="hero-prompts">
 089                    @foreach (var prompt in _dashboard.Prompts.Take(3))
 90                    {
 091                        <div class="hero-prompt-card @GetCategoryClass(prompt.Category)" @onclick="() => HandlePromptCli
 92                            <div class="prompt-icon">
 93                                <IconDisplay Icon="@prompt.Icon" DefaultIcon="💡" />
 94                            </div>
 95                            <div class="prompt-content">
 096                                <span class="prompt-title">@prompt.Title</span>
 097                                <span class="prompt-message">@prompt.Message</span>
 98                            </div>
 099                            @if (!string.IsNullOrEmpty(prompt.ActionUrl))
 100                            {
 101                                <MudIcon Icon="@Icons.Material.Filled.ChevronRight" Class="prompt-arrow" />
 102                            }
 103                        </div>
 104                    }
 105                </div>
 106            }
 107        </div>
 108
 109        <!-- Main Content -->
 110        <MudPaper Elevation="2" Class="dashboard-container">
 111            <div class="chronicis-dashboard">
 112                <MudGrid>
 113                    <!-- Main Content: World Panels -->
 114                    <MudItem xs="12" md="8">
 0115                        @if (_orderedWorlds.Any())
 116                        {
 117                            <!-- Primary World -->
 118                            <WorldPanel World="@_orderedWorlds.First()"
 119                                        IsExpanded="true" />
 120
 121                            <!-- Other Worlds -->
 0122                            @if (_orderedWorlds.Count > 1)
 123                            {
 124                                <MudText Typo="Typo.overline" Class="other-worlds-header">
 125                                    Other Worlds
 126                                </MudText>
 0127                                @foreach (var world in _orderedWorlds.Skip(1))
 128                                {
 129                                    <WorldPanel World="@world"
 130                                                IsExpanded="false"
 131                                                Class="mb-3" />
 132                                }
 133                            }
 134                        }
 135                        else
 136                        {
 137                            <!-- Empty State: No Worlds -->
 138                            <MudPaper Elevation="0" Class="empty-state-panel">
 139                                <div class="empty-state-content">
 140                                    <div class="empty-icon">🌍</div>
 141                                    <MudText Typo="Typo.h5" Class="empty-title">
 142                                        Begin Your Chronicle
 143                                    </MudText>
 144                                    <MudText Typo="Typo.body1" Class="empty-message">
 145                                        Create your first world to start organizing your campaign knowledge.
 146                                        Every great adventure needs a home.
 147                                    </MudText>
 148                                    <MudButton Variant="Variant.Filled"
 149                                               Color="Color.Primary"
 150                                               Size="Size.Large"
 151                                               StartIcon="@Icons.Material.Filled.Add"
 152                                               OnClick="CreateNewWorld"
 153                                               Class="mt-4">
 154                                        Create Your First World
 155                                    </MudButton>
 156                                </div>
 157                            </MudPaper>
 158                        }
 159                    </MudItem>
 160
 161                    <!-- Sidebar -->
 162                    <MudItem xs="12" md="4">
 163                        <!-- My Characters Summary -->
 0164                        @if (_dashboard.ClaimedCharacters.Any())
 165                        {
 166                            <MudPaper Elevation="1" Class="sidebar-panel mb-4">
 167                                <MudText Typo="Typo.h6" Class="panel-title">
 168                                    <MudIcon Icon="@Icons.Material.Filled.People" Class="mr-2" />
 169                                    My Characters
 170                                </MudText>
 171                                <div class="characters-summary">
 0172                                    @foreach (var character in _dashboard.ClaimedCharacters.Take(5))
 173                                    {
 0174                                        <div class="character-item" @onclick="async () => await NavigateToCharacter(char
 175                                            <IconDisplay Icon="@character.IconEmoji" DefaultIcon="👤" CssClass="characte
 176                                            <div class="character-info">
 0177                                                <span class="character-name">@character.Title</span>
 0178                                                <span class="character-world">@character.WorldName</span>
 179                                            </div>
 180                                        </div>
 181                                    }
 0182                                    @if (_dashboard.ClaimedCharacters.Count > 5)
 183                                    {
 184                                        <MudText Typo="Typo.caption" Class="more-characters">
 0185                                            +@(_dashboard.ClaimedCharacters.Count - 5) more characters
 186                                        </MudText>
 187                                    }
 188                                </div>
 189                            </MudPaper>
 190                        }
 191
 192                        <!-- Quick Stats -->
 193                        <MudPaper Elevation="1" Class="sidebar-panel stats-panel">
 194                            <MudText Typo="Typo.h6" Class="panel-title">
 195                                <MudIcon Icon="@Icons.Material.Filled.Analytics" Class="mr-2" />
 196                                Your Chronicle
 197                            </MudText>
 198                            <div class="stats-grid">
 199                                <div class="stat-box">
 0200                                    <span class="stat-number">@_dashboard.Worlds.Count</span>
 201                                    <span class="stat-label">Worlds</span>
 202                                </div>
 203                                <div class="stat-box">
 0204                                    <span class="stat-number">@_dashboard.Worlds.Sum(w => w.Campaigns.Count)</span>
 205                                    <span class="stat-label">Campaigns</span>
 206                                </div>
 207                                <div class="stat-box">
 0208                                    <span class="stat-number">@_dashboard.Worlds.Sum(w => w.ArticleCount)</span>
 209                                    <span class="stat-label">Articles</span>
 210                                </div>
 211                                <div class="stat-box">
 0212                                    <span class="stat-number">@_dashboard.ClaimedCharacters.Count</span>
 213                                    <span class="stat-label">Characters</span>
 214                                </div>
 215                            </div>
 216                        </MudPaper>
 217                    </MudItem>
 218                </MudGrid>
 219            </div>
 220        </MudPaper>
 221    </div>
 222}
 223
 224@code {
 225    private DashboardDto? _dashboard;
 0226    private List<DashboardWorldDto> _orderedWorlds = new();
 227    private Quote? _quote;
 0228    private bool _isLoading = true;
 229
 230    protected override async Task OnInitializedAsync()
 231    {
 232        // Check if user needs onboarding
 0233        var profile = await UserApi.GetUserProfileAsync();
 0234        if (profile != null && !profile.HasCompletedOnboarding)
 235        {
 0236            Navigation.NavigateTo("/getting-started", replace: true);
 0237            return;
 238        }
 239
 0240        TreeStateService.OnStateChanged += OnTreeStateChanged;
 241
 0242        await Task.WhenAll(
 0243            LoadDashboard(),
 0244            LoadQuote()
 0245        );
 0246    }
 247
 248    private async Task LoadDashboard()
 249    {
 0250        _isLoading = true;
 0251        StateHasChanged();
 252
 253        try
 254        {
 0255            _dashboard = await DashboardApi.GetDashboardAsync();
 256
 0257            if (_dashboard != null)
 258            {
 259                // Order worlds: prioritize those with active campaigns, then by most recent activity
 0260                _orderedWorlds = _dashboard.Worlds
 0261                    .OrderByDescending(w => w.Campaigns.Any(c => c.IsActive))
 0262                    .ThenByDescending(w => w.Campaigns
 0263                        .Where(c => c.CurrentArc?.LatestSessionDate != null)
 0264                        .Select(c => c.CurrentArc!.LatestSessionDate)
 0265                        .DefaultIfEmpty(DateTime.MinValue)
 0266                        .Max())
 0267                    .ThenByDescending(w => w.CreatedAt)
 0268                    .ToList();
 269            }
 0270        }
 0271        catch (Exception ex)
 272        {
 0273            _logger.LogErrorSanitized(ex, "Error loading dashboard");
 0274            Snackbar.Add("Failed to load dashboard", Severity.Error);
 0275        }
 276        finally
 277        {
 0278            _isLoading = false;
 0279            StateHasChanged();
 280        }
 0281    }
 282
 283    private async Task LoadQuote()
 284    {
 285        try
 286        {
 0287            _quote = await QuoteService.GetRandomQuoteAsync();
 0288        }
 0289        catch (Exception ex)
 290        {
 0291            _logger.LogErrorSanitized(ex, "Error loading quote");
 0292        }
 0293    }
 294
 295    private async Task CreateNewWorld()
 296    {
 297        try
 298        {
 0299            var createDto = new WorldCreateDto
 0300            {
 0301                Name = "New World",
 0302                Description = "A new world for your adventures"
 0303            };
 304
 0305            var world = await WorldApi.CreateWorldAsync(createDto);
 306
 0307            if (world != null)
 308            {
 0309                Snackbar.Add($"World '{world.Name}' created!", Severity.Success);
 0310                await TreeStateService.RefreshAsync();
 311
 0312                if (world.WorldRootArticleId.HasValue)
 313                {
 0314                    TreeStateService.ShouldFocusTitle = true;
 0315                    Navigation.NavigateTo($"/world/{world.Slug}");
 316                }
 317                else
 318                {
 0319                    await LoadDashboard();
 320                }
 321            }
 322            else
 323            {
 0324                Snackbar.Add("Failed to create world", Severity.Error);
 325            }
 0326        }
 0327        catch (Exception ex)
 328        {
 0329            _logger.LogErrorSanitized(ex, "Error creating world");
 0330            Snackbar.Add($"Error: {ex.Message}", Severity.Error);
 0331        }
 0332    }
 333
 334    private async Task JoinWorld()
 335    {
 0336        var dialog = await DialogService.ShowAsync<JoinWorldDialog>("Join a World");
 0337        var result = await dialog.Result;
 338
 0339        if (result != null && !result.Canceled && result.Data is Chronicis.Shared.DTOs.WorldJoinResultDto joinResult)
 340        {
 0341            Snackbar.Add($"Welcome to {joinResult.WorldName}!", Severity.Success);
 0342            await TreeStateService.RefreshAsync();
 0343            await LoadDashboard();
 344
 0345            if (joinResult.WorldId.HasValue)
 346            {
 0347                Navigation.NavigateTo($"/world/{joinResult.WorldId}");
 348            }
 349        }
 0350    }
 351
 352    private async Task NavigateToCharacter(Guid characterId)
 353    {
 0354        var article = await ArticleApi.GetArticleDetailAsync(characterId);
 0355        if (article != null && article.Breadcrumbs.Any())
 356        {
 0357            var path = string.Join("/", article.Breadcrumbs.Select(b => b.Slug));
 0358            Navigation.NavigateTo($"/article/{path}");
 359        }
 0360    }
 361
 362    private void HandlePromptClick(PromptDto prompt)
 363    {
 0364        if (!string.IsNullOrEmpty(prompt.ActionUrl))
 365        {
 0366            Navigation.NavigateTo(prompt.ActionUrl, forceLoad: true);
 367        }
 0368    }
 369
 370    private string GetCategoryClass(PromptCategory category)
 371    {
 0372        return category switch
 0373        {
 0374            PromptCategory.MissingFundamental => "missing-fundamental",
 0375            PromptCategory.NeedsAttention => "needs-attention",
 0376            PromptCategory.Suggestion => "suggestion",
 0377            _ => ""
 0378        };
 379    }
 380
 381    private void OnTreeStateChanged()
 382    {
 0383        InvokeAsync(StateHasChanged);
 0384    }
 385
 386    public void Dispose()
 387    {
 0388        TreeStateService.OnStateChanged -= OnTreeStateChanged;
 0389    }
 390}