Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/Setup/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ The appsettings.json file has a lot of options to customize the content of the b
"ContainerName": "",
"CdnEndpoint": ""
},
"UseMultiAuthorMode": false
"UseMultiAuthorMode": false,
"EnableTagDiscoveryPanel": true
}
```

Expand Down Expand Up @@ -113,3 +114,4 @@ The appsettings.json file has a lot of options to customize the content of the b
| ContainerName | string | The container name for the image storage provider |
| CdnEndpoint | string | Optional CDN endpoint to use for uploaded images. If set, the blog will return this URL instead of the storage account URL for uploaded assets. |
| UseMultiAuthorMode | boolean | The default value is `false`. If set to `true` then author name will be associated with blog posts at the time of creation. This author name will be fetched from the identity provider's `name` or `nickname` or `preferred_username` claim property. |
| EnableTagDiscoveryPanel | boolean | The default value is `true`. Enables the Tag Discovery Panel, which helps users discover topics by browsing popular tags. |
2 changes: 2 additions & 0 deletions src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public sealed record ApplicationConfiguration
public bool ShowBuildInformation { get; init; } = true;

public bool UseMultiAuthorMode { get; init; }

public bool EnableTagDiscoveryPanel { get; set; }
}
24 changes: 24 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using LinkDotNet.Blog.Web.Features.SupportMe.Components
@using LinkDotNet.Blog.Web.Features.TagDiscovery
@inject IOptions<ApplicationConfiguration> Configuration
@inject IOptions<SupportMeConfiguration> SupportConfiguration
@inject NavigationManager NavigationManager
Expand Down Expand Up @@ -57,17 +58,30 @@

<AccessControl CurrentUri="@currentUri"></AccessControl>
<li class="nav-item d-flex align-items-center"><ThemeToggler Class="nav-link"></ThemeToggler></li>

@if (Configuration.Value.EnableTagDiscoveryPanel)
{
<li class="nav-item d-flex align-items-center me-lg-2 mb-2 mb-lg-0">
<a class="nav-link d-flex align-items-center justify-content-center" @onclick="ToggleTagDiscoveryPanel"
style="font-family: 'icons'; font-weight: 900; cursor: pointer;"
title="Discover new topics"> &#xE936; </a>
</li>
}

<li class="d-flex">
<SearchInput SearchEntered="NavigateToSearchPage"></SearchInput>
</li>
</ul>
</div>
</div>
</nav>
<TagDiscoveryPanel IsOpen="@_isOpen" OnClose="CloseTagDiscoveryPanel" />

@code {
private string currentUri = string.Empty;

private bool _isOpen;

protected override void OnInitialized()
{
NavigationManager.LocationChanged += UpdateUri;
Expand All @@ -90,4 +104,14 @@
currentUri = e.Location;
StateHasChanged();
}

private void ToggleTagDiscoveryPanel()
{
_isOpen = !_isOpen;
}

private void CloseTagDiscoveryPanel()
{
_isOpen = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace LinkDotNet.Blog.Web.Features.Services.Tags;

public interface ITagQueryService
{
Task<IReadOnlyList<TagCount>> GetAllOrderedByUsageAsync();
}
3 changes: 3 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Services/Tags/TagCount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace LinkDotNet.Blog.Web.Features.Services.Tags;

public sealed record TagCount(string Name, int Count);
49 changes: 49 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Services/Tags/TagQueryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
using Microsoft.Extensions.Options;
using Raven.Client.Documents.Operations.AI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ZiggyCreatures.Caching.Fusion;

namespace LinkDotNet.Blog.Web.Features.Services.Tags;

public sealed class TagQueryService(
IRepository<BlogPost> blogPostRepository,
IFusionCache fusionCache,
IOptions<ApplicationConfiguration> appConfiguration) : ITagQueryService
{
private const string TagCacheKey = "TagUsageList";

public async Task<IReadOnlyList<TagCount>> GetAllOrderedByUsageAsync()
{
return await fusionCache.GetOrSetAsync(
TagCacheKey,
async _ => await LoadTagsAsync(),
options =>
{
options.SetDuration(TimeSpan.FromMinutes(
appConfiguration.Value.FirstPageCacheDurationInMinutes));
});
}

private async Task<IReadOnlyList<TagCount>> LoadTagsAsync()
{
var posts = await blogPostRepository.GetAllAsync();

var tagCounts = posts
.SelectMany(p => p.Tags ?? Enumerable.Empty<string>())
.Where(tag => !string.IsNullOrEmpty(tag))
.GroupBy(tag => tag.Trim())
.Select(group => new TagCount(
group.Key,
group.Count()))
.OrderByDescending(tc => tc.Count)
.ThenBy(tc => tc.Name)
.ToList();
return tagCounts;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@using Microsoft.AspNetCore.Components.Web;
@inject ITagQueryService TagQueryService
@inject IOptions<ApplicationConfiguration> AppConfiguration
@inject NavigationManager Navigation


@if (AppConfiguration.Value.EnableTagDiscoveryPanel && IsOpen)
{
<div class="position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-25"
style="z-index:1040; backdrop-filter: blur(2px);"
@onclick="Close">
</div>

<div class="position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index:1050; pointer-events:none;">

<div @ref="_panelRef"
class="bg-body border rounded shadow p-3"
style="max-width: 400px; max-height: 70vh; overflow-y:auto; pointer-events:auto;"
tabindex="0"
@onkeydown="HandleKeyDown">

@if (_tags.Count == 0)
{
<div class="text-muted text-center py-2">
No tags available yet.
</div>
}
else
{
<div class="d-flex flex-wrap gap-2">
@foreach (var tag in _tags)
{
<span class="badge bg-secondary d-flex align-items-center gap-1"
style="cursor: pointer;"
@onclick="() => Navigate(tag.Name)">
@tag.Name
<span class="badge bg-light text-dark">@tag.Count</span>
</span>
}
</div>
}
</div>
</div>
}

@code {
[Parameter] public bool IsOpen { get; set; }
[Parameter] public EventCallback OnClose { get; set; }

private IReadOnlyList<TagCount> _tags = [];
private ElementReference _panelRef;

protected override async Task OnParametersSetAsync()
{
if (IsOpen)
{
_tags = await TagQueryService.GetAllOrderedByUsageAsync();
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (IsOpen)
{
await _panelRef.FocusAsync();
}
}

private async Task Close()
{
await OnClose.InvokeAsync();
}

private async Task Navigate(string tag)
{
var encoded = Uri.EscapeDataString(tag);
Navigation.NavigateTo($"/searchByTag/{encoded}");
await Close();
}

private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Escape")
{
await Close();
}
}
}
2 changes: 2 additions & 0 deletions src/LinkDotNet.Blog.Web/ServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services;
using LinkDotNet.Blog.Web.Features.Bookmarks;
using LinkDotNet.Blog.Web.Features.Services;
using LinkDotNet.Blog.Web.Features.Services.Tags;
using LinkDotNet.Blog.Web.RegistrationExtensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
Expand All @@ -26,6 +27,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped<IXmlWriter, XmlWriter>();
services.AddScoped<IFileProcessor, FileProcessor>();
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<ITagQueryService, TagQueryService>();

services.AddSingleton<CacheService>();
services.AddSingleton<ICacheInvalidator>(s => s.GetRequiredService<CacheService>());
Expand Down
3 changes: 2 additions & 1 deletion src/LinkDotNet.Blog.Web/_Imports.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@using System.Net.Http
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
Expand All @@ -11,3 +11,4 @@
@using LinkDotNet.Blog.Web
@using LinkDotNet.Blog.Web.Features.Components
@using Microsoft.Extensions.Options
@using LinkDotNet.Blog.Web.Features.Services.Tags
5 changes: 3 additions & 2 deletions src/LinkDotNet.Blog.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"ProfilePictureUrl": "assets/profile-picture.webp"
},
"ImageStorageProvider": "<Provider>",
"ImageStorage" : {
"ImageStorage": {
"AuthenticationMode": "Default",
"ConnectionString": "",
"ServiceUrl": "",
Expand All @@ -49,5 +49,6 @@
"ShowReadingIndicator": true,
"ShowSimilarPosts": true,
"ShowBuildInformation": true,
"UseMultiAuthorMode": false
"UseMultiAuthorMode": false,
"EnableTagDiscoveryPanel": true
}
2 changes: 1 addition & 1 deletion src/LinkDotNet.Blog.Web/wwwroot/css/basic.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:root, html[data-bs-theme='light'] {
:root, html[data-bs-theme='light'] {
/* Fonts */
--default-font: 'Calibri';
--code-font: 'Lucida Console', 'Courier New';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Linq;
using AngleSharp.Html.Dom;
using LinkDotNet.Blog.TestUtilities;
using LinkDotNet.Blog.Web.Features.Home.Components;
using LinkDotNet.Blog.Web.Features.Services;
using LinkDotNet.Blog.Web.Features.Services.Tags;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Linq;

namespace LinkDotNet.Blog.IntegrationTests.Web.Shared;

Expand All @@ -14,6 +15,13 @@ public class NavMenuTests : BunitContext
public NavMenuTests()
{
ComponentFactories.Add<ThemeToggler, ThemeTogglerStub>();

var tagQueryService = Substitute.For<ITagQueryService>();
Services.AddSingleton(tagQueryService);

Services.AddSingleton(
Options.Create(new ApplicationConfigurationBuilder().Build())
);
}

[Fact]
Expand Down
Loading