Skip to content

Feature: Bookmarks #403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5428428
implement bookmarks
Feb 23, 2025
4244410
clear storage on startup when in development
Feb 23, 2025
b396e1c
add conditional bookmark buttons & use onAfterRender lifecycle hook
Feb 23, 2025
a5df71e
move bookmarks link to rss
Feb 25, 2025
6bdb0d8
add bookmark button to post page
Feb 25, 2025
890744d
remove bookmark icon and change position
Feb 25, 2025
720ef5d
remove localStorage.js
Feb 25, 2025
9f75386
remove unused dependencies
Feb 25, 2025
4a2d9ac
Fix CI/CD build issues
Feb 26, 2025
cf5922a
Create & use BookmarkButton, update bookmarkService
Feb 26, 2025
9f3b6a1
move bookmarks nav out of rss
Feb 27, 2025
3badad6
refactor BookmarkService
Feb 27, 2025
a697224
refactor bookmarkservice
Feb 28, 2025
220eb5a
update navmenu
Feb 28, 2025
8fef145
restyle bookmark button and add icon to navMenu
Feb 28, 2025
7233cab
add extra styling to bookmark button
Feb 28, 2025
1910223
add padding to bookmarks page
Feb 28, 2025
7548d03
add unit tests for BookmarkService
Feb 28, 2025
3eeca43
change exception type
Feb 28, 2025
91bc212
refactor code
Mar 1, 2025
0304be3
move BookmarkButton css to seperate file
Mar 2, 2025
71533ae
refactor
Mar 2, 2025
e590c40
Only update initially
linkdotnet Mar 7, 2025
52f3a8a
fix: Tests
linkdotnet Mar 7, 2025
7b4c994
Added tests
linkdotnet Mar 7, 2025
22fc52b
Updated bookmark implementation and icons
linkdotnet Mar 7, 2025
910707f
refactor: Remove unused objects
linkdotnet Mar 7, 2025
e7468e2
fix: Styling alignments
linkdotnet Mar 7, 2025
efa0e0f
fix: Remove debug stuff
linkdotnet Mar 7, 2025
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
21 changes: 11 additions & 10 deletions src/LinkDotNet.Blog.Web/App.razor
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
@using LinkDotNet.Blog.Web.Features.Home.Components

<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<ObjectNotFound></ObjectNotFound>
</LayoutView>
</NotFound>
</Router>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<ObjectNotFound></ObjectNotFound>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
63 changes: 63 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence.Sql;
using LinkDotNet.Blog.Web.Features.Services;
using Microsoft.EntityFrameworkCore;

namespace LinkDotNet.Blog.Web.Features.Bookmarks;

public class BookmarkService : IBookmarkService
{
private readonly ILocalStorageService localStorageService;

public BookmarkService(ILocalStorageService localStorageService)
{
this.localStorageService = localStorageService;
}

public async Task<bool> IsBookmarked(string postId)
{
ArgumentException.ThrowIfNullOrEmpty(postId);
await InitializeIfNotExists();
var bookmarks = await localStorageService.GetItemAsync<HashSet<string>>("bookmarks");

return bookmarks.Contains(postId);
}

public async Task<IReadOnlyList<string>> GetBookmarkedPostIds()
{
await InitializeIfNotExists();
return await localStorageService.GetItemAsync<IReadOnlyList<string>>("bookmarks");
}

public async Task SetBookmark(string postId, bool isBookmarked)
{
ArgumentException.ThrowIfNullOrEmpty(postId);
await InitializeIfNotExists();

var bookmarks = await localStorageService.GetItemAsync<HashSet<string>>("bookmarks");

if (!isBookmarked)
{
bookmarks.Remove(postId);
}
else
{
bookmarks.Add(postId);
}

await localStorageService.SetItemAsync("bookmarks", bookmarks);

}

private async Task InitializeIfNotExists()
{
if (!(await localStorageService.ContainKeyAsync("bookmarks")))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for consistency reasons: Can you wrap the if condition with brackets?

if (...) 
{
    ...
}

{
await localStorageService.SetItemAsync("bookmarks", new List<string>());
}
}
}
46 changes: 46 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@page "/bookmarks"
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Infrastructure.Persistence
@inject IBookmarkService BookmarkService
@inject IRepository<BlogPost> BlogPostRepository;

<div class="container">
<h3 class="pb-3 fw-bold">Bookmarks</h3>
@if (bookmarkedPosts.Count <= 0)
{
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">No bookmarks yet!</h4>
<p>You can bookmark posts while browsing by clicking the bookmark icon <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi text-secondary" viewBox="0 0 16 16">
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z"/>
</svg> that appears on each post.</p>
<hr>
<p class="mb-0">Bookmarks are stored in your browser's local storage and are not synchronized across devices or browsers.</p>
</div>
}
else
{
@foreach (var post in bookmarkedPosts)
{
<ShortBlogPost BlogPost="post" />
}
}
</div>

@code {
private IReadOnlyList<BlogPost> bookmarkedPosts = [];

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var ids = await BookmarkService.GetBookmarkedPostIds();

if (ids.Any())
{
bookmarkedPosts = await BlogPostRepository.GetAllByProjectionAsync(post => post, post => ids.Contains(post.Id));
StateHasChanged();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<button type="button" class="btn btn-sm bg-transparent border-0" @onclick="OnBookmarkClicked" title="@(IsBookmarked ? "Remove bookmark" : "Add bookmark")">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@(IsBookmarked ? "text-warning" : "text-secondary")" viewBox="0 0 16 16">
<path d="@(IsBookmarked ?
"M2 2v13.5a.5.5 0 0 0 .74.439L8 13.069l5.26 2.87A.5.5 0 0 0 14 15.5V2a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2z" :
"M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z")"/>
</svg>
</button>

@code {
[Parameter] public bool IsBookmarked { get; set; }
[Parameter] public EventCallback Bookmarked { get; set; }

private async Task OnBookmarkClicked()
{
await Bookmarked.InvokeAsync();
}
}
11 changes: 11 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace LinkDotNet.Blog.Web.Features.Bookmarks;

public interface IBookmarkService
{
public Task<bool> IsBookmarked(string postId);
public Task<IReadOnlyList<string>> GetBookmarkedPostIds();
public Task SetBookmark(string postId, bool isBookmarked);
}
29 changes: 26 additions & 3 deletions src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Web.Features.Bookmarks
@using LinkDotNet.Blog.Web.Features.Bookmarks.Components
@inject IBookmarkService BookmarkService

<article>
<div class="blog-card @AltCssClass">
Expand Down Expand Up @@ -33,11 +36,13 @@
</ul>
</div>
<div class="description">
<h1>@BlogPost.Title</h1>
<h2></h2>
<div class="header">
<h1>@BlogPost.Title</h1>
<BookmarkButton IsBookmarked="isBookmarked" Bookmarked="ToggleBookmark"></BookmarkButton>
</div>
<p>@MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)</p>
<p class="read-more">
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
</p>
</div>
</div>
Expand All @@ -47,6 +52,8 @@
[Parameter, EditorRequired]
public required BlogPost BlogPost { get; set; }

private bool isBookmarked = false;

[Parameter]
public bool UseAlternativeStyle { get; set; }

Expand All @@ -55,6 +62,13 @@

private string AltCssClass => UseAlternativeStyle ? "alt" : string.Empty;

private async Task ToggleBookmark()
{
isBookmarked = !isBookmarked;
await BookmarkService.SetBookmark(BlogPost.Id, isBookmarked);
StateHasChanged();
}

public override Task SetParametersAsync(ParameterView parameters)
{
foreach (var parameter in parameters)
Expand All @@ -75,4 +89,13 @@

return base.SetParametersAsync(ParameterView.Empty);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id);
StateHasChanged();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
z-index: 1;
}

.blog-card .description .header {
display: flex;
justify-content: space-between;
}

.blog-card .description h1 {
line-height: 1;
margin: 0 0 5px 0;
Expand Down
12 changes: 7 additions & 5 deletions src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 me-5">
<li><a class="nav-link" href="/"><i class="home"></i> Home</a></li>
<li><a class="nav-link" href="/archive"><i class="books"></i> Archive</a></li>
@if (Configuration.Value.IsAboutMeEnabled)
<li><a class="nav-link" href="/"><i class="home"></i> Home</a></li>
<li><a class="nav-link" href="/archive"><i class="books"></i> Archive</a></li>
<li><a class="nav-link" href="/bookmarks"><i class="bookmark"></i> Bookmarks</a>
</li>
@if (Configuration.Value.IsAboutMeEnabled)
{
<li class="nav-item">
<a class="nav-link" href="AboutMe">
Expand All @@ -48,8 +50,8 @@
<i class="rss2"></i> RSS
</a>
<ul class="dropdown-menu" aria-labelledby="rssDropdown">
<li><a class="dropdown-item" href="/feed.rss" aria-label="RSS with All Posts">All Posts (Summary)</a></li>
<li><a class="dropdown-item" href="/feed.rss?withContent=true" aria-label="RSS with Full Content">Most Recent Posts (Full Content)</a></li>
<li><a class="dropdown-item" href="/feed.rss" aria-label="RSS with All Posts">All Posts (Summary)</a></li>
<li><a class="dropdown-item" href="/feed.rss?withContent=true" aria-label="RSS with Full Content">Most Recent Posts (Full Content)</a></li>
</ul>
</li>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
@using Markdig
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Infrastructure.Persistence
@using LinkDotNet.Blog.Web.Features.Bookmarks
@using LinkDotNet.Blog.Web.Features.Services
@using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components
@using LinkDotNet.Blog.Web.Features.SupportMe.Components
@using LinkDotNet.Blog.Web.Features.Bookmarks.Components
@inject IRepository<BlogPost> BlogPostRepository
@inject IRepository<ShortCode> ShortCodeRepository
@inject IJSRuntime JsRuntime
Expand All @@ -14,6 +16,7 @@
@inject IOptions<ApplicationConfiguration> AppConfiguration
@inject IOptions<ProfileInformation> ProfileInformation
@inject IOptions<SupportMeConfiguration> SupportConfiguration
@inject IBookmarkService BookmarkService

@if (isLoading)
{
Expand All @@ -25,7 +28,7 @@ else if (!isLoading && BlogPost is null)
}
else if (BlogPost is not null)
{
<PageTitle>@BlogPost.Title</PageTitle>
<PageTitle>@BlogPost.Title</PageTitle>
<OgData Title="@BlogPost.Title"
AbsolutePreviewImageUrl="@OgDataImage"
Description="@(Markdown.ToPlainText(BlogPost.ShortDescription))"
Expand All @@ -49,7 +52,10 @@ else if (BlogPost is not null)
<span class="ms-1">@BlogPost.UpdatedDate.ToShortDateString()</span>
</div>
<span class="read-time"></span>
<span class="me-2">@BlogPost.ReadingTimeInMinutes minute read</span>
<span class="me-2">@BlogPost.ReadingTimeInMinutes minute read</span>
<div class="d-flex align-items-center">
<BookmarkButton IsBookmarked="isBookmarked" Bookmarked="BlogPostBookmarked"></BookmarkButton>
</div>
@if (BlogPost.Tags is not null && BlogPost.Tags.Any())
{
<div class="d-flex align-items-center">
Expand Down Expand Up @@ -110,6 +116,7 @@ else if (BlogPost is not null)
private string OgDataImage => BlogPost!.PreviewImageUrlFallback ?? BlogPost.PreviewImageUrl;
private string BlogPostCanoncialUrl => $"blogPost/{BlogPost?.Id}";
private IReadOnlyCollection<ShortCode> shortCodes = [];
private bool isBookmarked;

private BlogPost? BlogPost { get; set; }

Expand All @@ -129,6 +136,12 @@ else if (BlogPost is not null)
{
await JsRuntime.InvokeVoidAsync("hljs.highlightAll");
_ = UserRecordService.StoreUserRecordAsync();

if (BlogPost is not null && firstRender)
{
isBookmarked = await BookmarkService.IsBookmarked(BlogPost.Id);
StateHasChanged();
}
}

private MarkupString EnrichWithShortCodes(string content)
Expand All @@ -154,4 +167,17 @@ else if (BlogPost is not null)
BlogPost.Likes = hasLiked ? BlogPost.Likes + 1 : BlogPost.Likes - 1;
await BlogPostRepository.StoreAsync(BlogPost);
}

private async Task BlogPostBookmarked()
{
if (BlogPost is null)
{
return;
}

isBookmarked = !isBookmarked;
await BookmarkService.SetBookmark(BlogPost.Id, isBookmarked);
StateHasChanged();
}

}
2 changes: 1 addition & 1 deletion src/LinkDotNet.Blog.Web/Pages/_Host.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
Layout = "_Layout";
}

<component type="typeof(App)" render-mode="ServerPrerendered" />
<component type="typeof(App)" render-mode="ServerPrerendered" />
2 changes: 2 additions & 0 deletions src/LinkDotNet.Blog.Web/ServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Blazorise.Bootstrap5;
using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services;
using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services;
using LinkDotNet.Blog.Web.Features.Bookmarks;
using LinkDotNet.Blog.Web.Features.Services;
using LinkDotNet.Blog.Web.RegistrationExtensions;
using Microsoft.AspNetCore.Builder;
Expand All @@ -19,6 +20,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped<ILocalStorageService, LocalStorageService>();
services.AddScoped<ISortOrderCalculator, SortOrderCalculator>();
services.AddScoped<IUserRecordService, UserRecordService>();
services.AddScoped<IBookmarkService, BookmarkService>();
services.AddScoped<ISitemapService, SitemapService>();
services.AddScoped<IXmlWriter, XmlWriter>();
services.AddScoped<IFileProcessor, FileProcessor>();
Expand Down
10 changes: 6 additions & 4 deletions src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1929,7 +1929,10 @@
"ligatures": "bookmark, ribbon",
"name": "bookmark",
"id": 210,
"order": 0
"order": 87,
"prevSize": 32,
"code": 59858,
"tempChar": ""
},
{
"ligatures": "bookmarks, ribbons",
Expand Down Expand Up @@ -3150,7 +3153,7 @@
"order": 85,
"prevSize": 32,
"code": 60061,
"tempChar": ""
"tempChar": ""
},
{
"ligatures": "youtube2, brand22",
Expand Down Expand Up @@ -13357,6 +13360,5 @@
"showCodes": true,
"gridSize": 16
},
"uid": -1,
"time": 1731146188893
"uid": -1
}
Binary file modified src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff
Binary file not shown.
Binary file modified src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2
Binary file not shown.
Loading