< Summary

Information
Class: Chronicis.Client.Pages.Cosmos
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/Cosmos.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 145
Coverable lines: 145
Total lines: 488
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 74
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1@page "/articosmoscle/{*Path}"
 2@attribute [Authorize]
 3@inject ITreeStateService TreeStateService
 4@inject IArticleApiService ArticleApi
 5@inject IQuoteService QuoteService
 6@inject IBreadcrumbService BreadcrumbService
 7@inject NavigationManager Navigation
 8@implements IDisposable
 9
 10@code {
 11    private readonly ILogger<Cosmos> _logger;
 12
 13    // Direct constructor injection
 014    public Cosmos(ILogger<Cosmos> logger)
 15    {
 016        _logger = logger;
 017    }
 18}
 19
 020@if (TreeStateService.SelectedArticleId.HasValue && TreeStateService.SelectedArticleId.Value != Guid.Empty)
 21{
 22    <ArticleDetail />
 23}
 24else
 25{
 26    <div class="chronicis-welcome-page">
 27        <!-- Hero Section -->
 28        <MudPaper Elevation="0" Class="chronicis-hero pa-8 mb-6" Style="background: linear-gradient(135deg, rgba(31, 42,
 29            <div class="text-center">
 30                <div style="font-size: 4rem; margin-bottom: 16px;">📖✨</div>
 31                <MudText Typo="Typo.h2" Class="mb-3" Style="font-family: 'Spellweaver Display', serif; color: #C4AF8E;">
 32                    Your Chronicle Awaits
 33                </MudText>
 34                <MudText Typo="Typo.h6" Class="mb-4" Style="opacity: 0.9; max-width: 600px; margin: 0 auto;">
 35                    Every great campaign deserves a legendary chronicle. Organize your world, track your story, and neve
 36                </MudText>
 37                <MudButton Variant="Variant.Filled"
 38                           Color="Color.Primary"
 39                           Size="Size.Large"
 40                           StartIcon="@Icons.Material.Filled.Add"
 41                           OnClick="CreateFirstArticle"
 42                           Class="mt-2">
 43                    Start Your First Article
 44                </MudButton>
 45            </div>
 46        </MudPaper>
 47
 48        <!-- Stats Cards -->
 049        @if (_stats != null)
 50        {
 51            <MudGrid Class="mb-6">
 52                <MudItem xs="12" sm="6" md="3">
 53                    <MudPaper Elevation="2" Class="pa-4 text-center chronicis-stat-card">
 54                        <div style="font-size: 2.5rem; color: #C4AF8E;">📚</div>
 055                        <MudText Typo="Typo.h4" Class="mt-2">@_stats.TotalArticles</MudText>
 56                        <MudText Typo="Typo.body2" Color="Color.Secondary">Total Articles</MudText>
 57                    </MudPaper>
 58                </MudItem>
 59                <MudItem xs="12" sm="6" md="3">
 60                    <MudPaper Elevation="2" Class="pa-4 text-center chronicis-stat-card">
 61                        <div style="font-size: 2.5rem; color: #C4AF8E;">🗂️</div>
 062                        <MudText Typo="Typo.h4" Class="mt-2">@_stats.RootArticles</MudText>
 63                        <MudText Typo="Typo.body2" Color="Color.Secondary">Top-Level Topics</MudText>
 64                    </MudPaper>
 65                </MudItem>
 66                <MudItem xs="12" sm="6" md="3">
 67                    <MudPaper Elevation="2" Class="pa-4 text-center chronicis-stat-card">
 68                        <div style="font-size: 2.5rem; color: #C4AF8E;">✍️</div>
 069                        <MudText Typo="Typo.h4" Class="mt-2">@_stats.RecentlyModified</MudText>
 70                        <MudText Typo="Typo.body2" Color="Color.Secondary">Edited This Week</MudText>
 71                    </MudPaper>
 72                </MudItem>
 73                <MudItem xs="12" sm="6" md="3">
 74                    <MudPaper Elevation="2" Class="pa-4 text-center chronicis-stat-card">
 75                        <div style="font-size: 2.5rem; color: #C4AF8E;">🎲</div>
 076                        <MudText Typo="Typo.h4" Class="mt-2">@_stats.DaysSinceStart</MudText>
 77                        <MudText Typo="Typo.body2" Color="Color.Secondary">Days Chronicling</MudText>
 78                    </MudPaper>
 79                </MudItem>
 80            </MudGrid>
 81        }
 82
 83        <!-- Main Content Grid -->
 84        <MudGrid>
 85            <!-- Recent Articles -->
 86            <MudItem xs="12" md="8">
 87                <MudPaper Elevation="2" Class="pa-6">
 88                    <div class="d-flex align-center justify-space-between mb-4">
 89                        <MudText Typo="Typo.h5" Style="color: #C4AF8E;">
 90                            📜 Recent Articles
 91                        </MudText>
 092                        @if (_recentArticles?.Any() ?? false)
 93                        {
 94                            <MudButton Variant="Variant.Text"
 95                                       Color="Color.Primary"
 96                                       Size="Size.Small">
 97                                View All
 98                            </MudButton>
 99                        }
 100                    </div>
 101
 0102                    @if (_isLoadingRecent)
 103                    {
 104                        <div class="text-center py-4">
 105                            <MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true" />
 106                        </div>
 107                    }
 0108                    else if (_recentArticles?.Any() ?? false)
 109                    {
 110                        <MudList T="string">
 0111                            @foreach (var article in _recentArticles)
 112                            {
 0113                                <MudListItem T="string" OnClick="@(() => NavigateToArticle(article.Id))">
 114                                    <div class="d-flex align-center">
 115                                        <MudIcon Icon="@(string.IsNullOrEmpty(article.IconEmoji) ? Icons.Material.Filled
 116                                                 Color="Color.Primary"
 117                                                 Class="mr-3" />
 118                                        <div style="flex: 1;">
 119                                            <MudText Typo="Typo.body1">
 0120                                                @(string.IsNullOrEmpty(article.Title) ? "(Untitled)" : article.Title)
 121                                            </MudText>
 122                                            <MudText Typo="Typo.caption" Color="Color.Secondary">
 0123                                                Modified @FormatRelativeTime(article.ModifiedAt.HasValue ? article.Modif
 124                                            </MudText>
 125                                        </div>
 126                                        <MudIcon Icon="@Icons.Material.Filled.ChevronRight" Size="Size.Small" />
 127                                    </div>
 128                                </MudListItem>
 129                                <MudDivider />
 130                            }
 131                        </MudList>
 132                    }
 133                    else
 134                    {
 135                        <div class="text-center py-6" style="opacity: 0.6;">
 136                            <div style="font-size: 3rem; margin-bottom: 12px;">📝</div>
 137                            <MudText Typo="Typo.body1" Color="Color.Secondary">
 138                                No articles yet. Start building your chronicle!
 139                            </MudText>
 140                        </div>
 141                    }
 142                </MudPaper>
 143            </MudItem>
 144
 145            <!-- Quick Actions & Tips -->
 146            <MudItem xs="12" md="4">
 147                <!-- Quick Actions -->
 148                <MudPaper Elevation="2" Class="pa-6 mb-4">
 149                    <MudText Typo="Typo.h6" Class="mb-3" Style="color: #C4AF8E;">
 150                        ⚡ Quick Actions
 151                    </MudText>
 152                    <MudStack Spacing="2">
 153                        <MudButton Variant="Variant.Outlined"
 154                                   Color="Color.Primary"
 155                                   FullWidth="true"
 156                                   StartIcon="@Icons.Material.Filled.PersonAdd"
 0157                                   OnClick="@(() => CreateArticleWithTitle("New Character"))">
 158                            Create Character
 159                        </MudButton>
 160                        <MudButton Variant="Variant.Outlined"
 161                                   Color="Color.Primary"
 162                                   FullWidth="true"
 163                                   StartIcon="@Icons.Material.Filled.Place"
 0164                                   OnClick="@(() => CreateArticleWithTitle("New Location"))">
 165                            Add Location
 166                        </MudButton>
 167                        <MudButton Variant="Variant.Outlined"
 168                                   Color="Color.Primary"
 169                                   FullWidth="true"
 170                                   StartIcon="@Icons.Material.Filled.CalendarToday"
 0171                                   OnClick="@(() => CreateArticleWithTitle("Session Notes"))">
 172                            Session Notes
 173                        </MudButton>
 174                        <MudButton Variant="Variant.Outlined"
 175                                   Color="Color.Primary"
 176                                   FullWidth="true"
 177                                   StartIcon="@Icons.Material.Filled.AutoStories"
 0178                                   OnClick="@(() => CreateArticleWithTitle("Lore Entry"))">
 179                            Lore Entry
 180                        </MudButton>
 181                    </MudStack>
 182                </MudPaper>
 183
 184                <!-- Tips Card -->
 185                <MudPaper Elevation="2" Class="pa-6" Style="background: linear-gradient(135deg, rgba(196, 175, 142, 0.1)
 186                    <MudText Typo="Typo.h6" Class="mb-3" Style="color: #C4AF8E;">
 187                        💡 Pro Tips
 188                    </MudText>
 189                    <MudStack Spacing="3">
 190                        <div>
 191                            <MudText Typo="Typo.body2" Class="mb-1">
 192                                <strong>Search Tree:</strong>
 193                            </MudText>
 194                            <MudText Typo="Typo.caption" Color="Color.Secondary">
 195                                Use the search box in the sidebar to filter articles by title
 196                            </MudText>
 197                        </div>
 198                        <MudDivider />
 199                        <div>
 200                            <MudText Typo="Typo.body2" Class="mb-1">
 201                                <strong>Organize with Hierarchy:</strong>
 202                            </MudText>
 203                            <MudText Typo="Typo.caption" Color="Color.Secondary">
 204                                Use the three-dot menu to add child articles and build your knowledge tree
 205                            </MudText>
 206                        </div>
 207                        <MudDivider />
 208                        <div>
 209                            <MudText Typo="Typo.body2" Class="mb-1">
 210                                <strong>Auto-Save:</strong>
 211                            </MudText>
 212                            <MudText Typo="Typo.caption" Color="Color.Secondary">
 213                                Your work saves automatically as you type—no need to click Save!
 214                            </MudText>
 215                        </div>
 216                    </MudStack>
 217                </MudPaper>
 218            </MudItem>
 219        </MudGrid>
 220
 221        <!-- Inspirational Quote Footer -->
 222        <MudPaper Elevation="0" Class="pa-6 mt-6 text-center chronicis-quote-footer" Style="background: rgba(196, 175, 1
 0223            @if (_loadingQuote)
 224            {
 225                <MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true" />
 226            }
 0227            else if (_quote != null)
 228            {
 229                <MudText Typo="Typo.body1" Style="font-style: italic; color: #3A4750;">
 0230                    "@_quote.Content"
 231                </MudText>
 232                <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
 0233                    — @_quote.Author
 234                </MudText>
 235                <MudTooltip Text="Get a new quote">
 236                    <MudIconButton Icon="@Icons.Material.Filled.Refresh"
 237                                   Size="Size.Small"
 238                                   Color="Color.Primary"
 239                                   OnClick="LoadNewQuote"
 240                                   Class="mt-2"
 241                                   Style="opacity: 0.6;" />
 242                </MudTooltip>
 243            }
 244        </MudPaper>
 245    </div>
 246}
 247
 248@code {
 249    [Parameter]
 0250    public string? Path { get; set; }
 251
 252    private CampaignStats? _stats;
 253    private List<ArticleDto>? _recentArticles;
 0254    private bool _isLoadingRecent = true;
 255    private Quote? _quote;
 0256    private bool _loadingQuote = true;
 257
 258    protected override async Task OnInitializedAsync()
 259    {
 0260        TreeStateService.OnStateChanged += OnTreeStateChanged;
 261
 262        // If URL has article path, load it
 0263        if (!string.IsNullOrEmpty(Path))
 264        {
 0265            await LoadArticleByPath(Path);
 266        }
 267
 0268        await Task.WhenAll(
 0269            LoadDashboardData(),
 0270            LoadQuote()
 0271        );
 0272    }
 273
 274    protected override async Task OnParametersSetAsync()
 275    {
 276        // Handle URL changes (browser back/forward)
 0277        if (!string.IsNullOrEmpty(Path))
 278        {
 0279            await LoadArticleByPath(Path);
 280        }
 0281    }
 282
 283    private async Task LoadArticleByPath(string path)
 284    {
 285        try
 286        {
 0287            var article = await ArticleApi.GetArticleByPathAsync(path);
 288
 0289            if (article != null)
 290            {
 0291                if (article.Id != TreeStateService.SelectedArticleId)
 292                {
 0293                    TreeStateService.ExpandPathToAndSelect(article.Id);
 294                }
 295            }
 296            else
 297            {
 298                // Article not found
 0299                if (!TreeStateService.SelectedArticleId.HasValue)
 300                {
 0301                    _logger.LogWarningSanitized("Article not found for path: {Path}", path);
 0302                    Navigation.NavigateTo("/", replace: true);
 303                }
 304            }
 0305        }
 0306        catch (Exception ex)
 307        {
 0308            _logger.LogErrorSanitized(ex, "Error loading article by path: {Path}", path);
 0309            if (!TreeStateService.SelectedArticleId.HasValue)
 310            {
 0311                Navigation.NavigateTo("/", replace: true);
 312            }
 0313        }
 0314    }
 315
 316    private async Task LoadQuote()
 317    {
 0318        _loadingQuote = true;
 0319        StateHasChanged();
 320
 321        try
 322        {
 0323            _quote = await QuoteService.GetRandomQuoteAsync();
 0324        }
 0325        catch (Exception ex)
 326        {
 0327            _logger.LogErrorSanitized(ex, "Error loading quote");
 328            // Fallback quote already handled in service
 0329        }
 330        finally
 331        {
 0332            _loadingQuote = false;
 0333            StateHasChanged();
 334        }
 0335    }
 336
 337    private async Task LoadNewQuote()
 338    {
 0339        await LoadQuote();
 0340    }
 341
 342    private async Task LoadDashboardData()
 343    {
 0344        _isLoadingRecent = true;
 0345        StateHasChanged();
 346
 347        try
 348        {
 349            // Load recent articles (top 5 by modified date)
 0350            var allArticles = await ArticleApi.GetRootArticlesAsync();
 0351            _recentArticles = await GetRecentArticlesRecursive(allArticles);
 352
 353            // Calculate stats
 0354            _stats = new CampaignStats
 0355            {
 0356                TotalArticles = CountTotalArticles(allArticles),
 0357                RootArticles = allArticles.Count,
 0358                RecentlyModified = _recentArticles.Count(a =>
 0359                    (a.ModifiedAt.HasValue ? a.ModifiedAt.Value : a.CreatedAt) > DateTime.Now.AddDays(-7)),
 0360                DaysSinceStart = allArticles.Any()
 0361                    ? (int)(DateTime.Now - allArticles.Min(a => a.CreatedAt)).TotalDays
 0362                    : 0
 0363            };
 0364        }
 0365        catch (Exception ex)
 366        {
 0367            _logger.LogErrorSanitized(ex, "Error loading dashboard data");
 0368        }
 369        finally
 370        {
 0371            _isLoadingRecent = false;
 0372            StateHasChanged();
 373        }
 0374    }
 375
 376    private async Task<List<ArticleDto>> GetRecentArticlesRecursive(List<ArticleTreeDto> articles, int maxResults = 5)
 377    {
 0378        var allArticles = new List<ArticleDto>();
 379
 0380        foreach (var article in articles)
 381        {
 382            // Convert TreeDto to full Dto
 0383            var fullArticle = await ArticleApi.GetArticleAsync(article.Id);
 0384            if (fullArticle != null)
 385            {
 0386                allArticles.Add(fullArticle);
 387            }
 388
 389            // Recursively get children
 0390            if (article.HasChildren && article.Children != null)
 391            {
 0392                var childArticles = await GetRecentArticlesRecursive(article.Children.ToList(), maxResults);
 0393                allArticles.AddRange(childArticles);
 394            }
 0395        }
 396
 0397        return allArticles
 0398            .OrderByDescending(a => a.ModifiedAt.HasValue ? a.ModifiedAt.Value : a.CreatedAt)
 0399            .Take(maxResults)
 0400            .ToList();
 0401    }
 402
 403    private int CountTotalArticles(List<ArticleTreeDto> articles)
 404    {
 0405        int count = articles.Count;
 0406        foreach (var article in articles)
 407        {
 0408            if (article.Children != null)
 409            {
 0410                count += CountTotalArticles(article.Children.ToList());
 411            }
 412        }
 0413        return count;
 414    }
 415
 416    private async Task NavigateToArticle(Guid articleId)
 417    {
 418        // Get the full article details to build path from breadcrumbs
 0419        var article = await ArticleApi.GetArticleDetailAsync(articleId);
 0420        if (article != null && article.Breadcrumbs.Any())
 421        {
 0422            var path = BreadcrumbService.BuildArticleUrl(article.Breadcrumbs);
 0423            Navigation.NavigateTo(path);
 424        }
 0425    }
 426
 427    private async Task CreateFirstArticle()
 428    {
 0429        await CreateArticleWithTitle(string.Empty);
 0430    }
 431
 432    private async Task CreateArticleWithTitle(string title)
 433    {
 0434        var createDto = new ArticleCreateDto
 0435        {
 0436            Title = title,
 0437            Body = string.Empty,
 0438            ParentId = null,
 0439            EffectiveDate = DateTime.Now
 0440        };
 441
 0442        var created = await ArticleApi.CreateArticleAsync(createDto);
 0443        if (created == null)
 444        {
 0445            _logger.LogErrorSanitized("Failed to create article");
 0446            return;
 447        }
 448
 0449        await TreeStateService.RefreshAsync();
 0450        TreeStateService.ExpandPathToAndSelect(created.Id);
 0451    }
 452
 453    private string FormatRelativeTime(DateTime dateTime)
 454    {
 0455        var timeSpan = DateTime.Now - dateTime;
 456
 0457        if (timeSpan.TotalMinutes < 1)
 0458            return "just now";
 0459        if (timeSpan.TotalMinutes < 60)
 0460            return $"{(int)timeSpan.TotalMinutes}m ago";
 0461        if (timeSpan.TotalHours < 24)
 0462            return $"{(int)timeSpan.TotalHours}h ago";
 0463        if (timeSpan.TotalDays < 7)
 0464            return $"{(int)timeSpan.TotalDays}d ago";
 0465        if (timeSpan.TotalDays < 30)
 0466            return $"{(int)(timeSpan.TotalDays / 7)}w ago";
 467
 0468        return dateTime.ToString("MMM d, yyyy");
 469    }
 470
 471    private void OnTreeStateChanged()
 472    {
 0473        InvokeAsync(StateHasChanged);
 0474    }
 475
 476    public void Dispose()
 477    {
 0478        TreeStateService.OnStateChanged -= OnTreeStateChanged;
 0479    }
 480
 481    private class CampaignStats
 482    {
 0483        public int TotalArticles { get; set; }
 0484        public int RootArticles { get; set; }
 0485        public int RecentlyModified { get; set; }
 0486        public int DaysSinceStart { get; set; }
 487    }
 488}