Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 4 additions & 0 deletions src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ public sealed record ApplicationConfiguration
public bool ShowBuildInformation { get; init; } = true;

public bool UseMultiAuthorMode { get; init; }

public bool EnableTagDiscoveryPanel { get; set; }

public bool ShowTagsWithCountInTagDiscovery { get; set; }
}
23 changes: 23 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,6 +58,16 @@

<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">
<a class="tag-discovery-btn" @onclick="ToggleTagDiscoveryPanel"
title="Discover new topics"> &#xE936; </a>
<TagDiscoveryPanel IsOpen="@_isOpen" OnClose="CloseTagDiscoveryPanel" />
</li>
}

<li class="d-flex">
<SearchInput SearchEntered="NavigateToSearchPage"></SearchInput>
</li>
Expand All @@ -68,6 +79,8 @@
@code {
private string currentUri = string.Empty;

private bool _isOpen;

protected override void OnInitialized()
{
NavigationManager.LocationChanged += UpdateUri;
Expand All @@ -90,4 +103,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);
40 changes: 40 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,40 @@
using Azure.Storage.Blobs.Models;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

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

public sealed class TagQueryService(IRepository<BlogPost> blogPostRepository) : ITagQueryService
{
public async Task<IReadOnlyList<TagCount>> GetAllOrderedByUsageAsync()
{
var posts = await blogPostRepository.GetAllAsync();

var tagCounts = posts
// Flatten the collection of tag lists into a single sequence.
Comment thread
EmanuelGF marked this conversation as resolved.
Outdated
.SelectMany(p => p.Tags ?? Enumerable.Empty<string>())

// Defensive guard against invalid tag values.
.Where(tag => !string.IsNullOrEmpty(tag))

.GroupBy(tag => tag.Trim())

// Transform each group into a TagCount DTO.
// group.Key = tag name
// group.Count() = number of occurrences
.Select(group => new TagCount(
group.Key,
group.Count()))

// Sort descending by usage count (most popular first).
.OrderByDescending(tc => tc.Count)
.ThenBy(tc => tc.Name)
.ToList();

return tagCounts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@inject ITagQueryService TagQueryService
@inject IOptions<ApplicationConfiguration> AppConfiguration
@inject NavigationManager Navigation

@if (!AppConfiguration.Value.EnableTagDiscoveryPanel || !IsOpen) { return; }

<div class="tag-overlay" @onclick="Close"></div>

<div class="tag-panel">
<div class="tag-discovery-container">
@foreach (var tag in _tags)
{
<span class="tag-badge" @onclick="() => Navigate(tag.Name)">
@tag.Name

@if (AppConfiguration.Value.ShowTagsWithCountInTagDiscovery)
{
<span class="tag-count">@tag.Count</span>
}
</span>
}
</div>
</div>

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

private IReadOnlyList<TagCount> _tags = [];

protected override async Task OnParametersSetAsync()
{
if (IsOpen && _tags.Count == 0)
Comment thread
EmanuelGF marked this conversation as resolved.
Outdated
{
_tags = await TagQueryService.GetAllOrderedByUsageAsync();
}
}

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

private async Task Navigate(string tag)
{
var encoded = Uri.EscapeDataString(tag);
Navigation.NavigateTo($"/searchByTag/{encoded}");
await Close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.tag-overlay {
Comment thread
EmanuelGF marked this conversation as resolved.
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(2px);
z-index: 1000;
}

.tag-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(340px, 92vw);
max-height: 70vh;
background: var(--background-color, #ffffff);
color: var(--text-color, #222);
border-radius: 14px;
padding: 1.2rem;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
z-index: 1001;
}

.tag-discovery-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}

.tag-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
background-color: #4f83cc;
color: white;
cursor: pointer;
transition: transform 0.1s ease, background-color 0.1s ease, box-shadow 0.1s ease;
}

.tag-badge:hover {
background-color: #3c6fb3;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}

.tag-count {
background: rgba(0, 0, 0, 0.25);
border-radius: 999px;
padding: 2px 7px;
font-size: 0.7rem;
font-weight: 600;
}

.tag-panel::-webkit-scrollbar {
width: 6px;
}

.tag-panel::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.25);
border-radius: 6px;
}

.no-scroll {
overflow: hidden;
}
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
6 changes: 4 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,7 @@
"ShowReadingIndicator": true,
"ShowSimilarPosts": true,
"ShowBuildInformation": true,
"UseMultiAuthorMode": false
"UseMultiAuthorMode": false,
"EnableTagDiscoveryPanel": true,
Comment thread
EmanuelGF marked this conversation as resolved.
Outdated
"ShowTagsWithCountInTagDiscovery": true
Comment thread
EmanuelGF marked this conversation as resolved.
Outdated
}
13 changes: 12 additions & 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 Expand Up @@ -645,6 +645,17 @@ code {
object-fit: cover;
}

.tag-discovery-btn {
font-family: 'icons';
font-weight: 900;
content: "\e936";
cursor: pointer;
padding: 6px;
margin: 6px;
text-decoration: none;
color: var(--bs-navbar-color);
}

@media only screen and (max-width: 700px) {
.blog-outer-box .blog-container {
width: 90%;
Expand Down
Loading