Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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();
Comment thread
EmanuelGF marked this conversation as resolved.
Outdated

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