From 0bad1385beb17cd1244579d59465fe17e948a99f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 08:17:50 +0000 Subject: [PATCH 001/682] Bump bunit from 1.14.4 to 1.15.5 Bumps [bunit](https://github.com/bUnit-dev/bUnit) from 1.14.4 to 1.15.5. - [Release notes](https://github.com/bUnit-dev/bUnit/releases) - [Changelog](https://github.com/bUnit-dev/bUnit/blob/main/CHANGELOG.md) - [Commits](https://github.com/bUnit-dev/bUnit/compare/v1.14.4...v1.15.5) --- updated-dependencies: - dependency-name: bunit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 60f9603b..40c4e8b0 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,7 @@ - + From ca70723335ca992f9923b64dc939c80e95bb3c43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 09:16:06 +0000 Subject: [PATCH 002/682] Bump FluentAssertions from 6.9.0 to 6.10.0 Bumps [FluentAssertions](https://github.com/fluentassertions/fluentassertions) from 6.9.0 to 6.10.0. - [Release notes](https://github.com/fluentassertions/fluentassertions/releases) - [Changelog](https://github.com/fluentassertions/fluentassertions/blob/develop/AcceptApiChanges.ps1) - [Commits](https://github.com/fluentassertions/fluentassertions/compare/6.9.0...6.10.0) --- updated-dependencies: - dependency-name: FluentAssertions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 40c4e8b0..39b54ba5 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -3,7 +3,7 @@ - + From c18a52111cc164c1879135d868339c23d2d9ccf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 09:20:09 +0000 Subject: [PATCH 003/682] Bump bunit from 1.15.5 to 1.16.2 Bumps [bunit](https://github.com/bUnit-dev/bUnit) from 1.15.5 to 1.16.2. - [Release notes](https://github.com/bUnit-dev/bUnit/releases) - [Changelog](https://github.com/bUnit-dev/bUnit/blob/main/CHANGELOG.md) - [Commits](https://github.com/bUnit-dev/bUnit/compare/v1.15.5...v1.16.2) --- updated-dependencies: - dependency-name: bunit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 39b54ba5..ff69fb6c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,7 @@ - + From a7c0ec21df0de86b442602388f522f4126b3ca24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 09:03:09 +0000 Subject: [PATCH 004/682] Bump RavenDB.Client from 5.4.100 to 5.4.101 Bumps [RavenDB.Client](https://github.com/ravendb/ravendb) from 5.4.100 to 5.4.101. - [Release notes](https://github.com/ravendb/ravendb/releases) - [Commits](https://github.com/ravendb/ravendb/compare/5.4.100...5.4.101) --- updated-dependencies: - dependency-name: RavenDB.Client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.Infrastructure.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj index c34cf9dd..63fd1a09 100644 --- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj +++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj @@ -8,7 +8,7 @@ - + From 7ddd7e7cd88e17ce794024f9162848a34eb8a69c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 09:03:17 +0000 Subject: [PATCH 005/682] Bump RavenDB.TestDriver from 5.4.100 to 5.4.101 Bumps [RavenDB.TestDriver](https://github.com/ravendb/ravendb) from 5.4.100 to 5.4.101. - [Release notes](https://github.com/ravendb/ravendb/releases) - [Commits](https://github.com/ravendb/ravendb/compare/5.4.100...5.4.101) --- updated-dependencies: - dependency-name: RavenDB.TestDriver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.IntegrationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index c5c1ee6b..f7f48625 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -8,7 +8,7 @@ - + From ca04970685942860b75c9a6477d2b314ae895ad6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:04:17 +0000 Subject: [PATCH 006/682] Bump Microsoft.EntityFrameworkCore from 7.0.2 to 7.0.3 Bumps [Microsoft.EntityFrameworkCore](https://github.com/dotnet/efcore) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.Infrastructure.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj index 63fd1a09..7b4ef5e3 100644 --- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj +++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj @@ -7,7 +7,7 @@ - + From 40cecc81d70b85c7e9d816c71a3684affdcccd6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:04:56 +0000 Subject: [PATCH 007/682] Bump Microsoft.AspNetCore.Mvc.Testing from 7.0.2 to 7.0.3 Bumps [Microsoft.AspNetCore.Mvc.Testing](https://github.com/dotnet/aspnetcore) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Mvc.Testing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.IntegrationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index f7f48625..0b2ff070 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -6,7 +6,7 @@ - + From 2c6fc3cf1e18ca0a855a06c8ef1aea70719c6352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:04:43 +0000 Subject: [PATCH 008/682] Bump Microsoft.EntityFrameworkCore.SqlServer from 7.0.2 to 7.0.3 Bumps [Microsoft.EntityFrameworkCore.SqlServer](https://github.com/dotnet/efcore) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.SqlServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 05a474cb..eab35d01 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -12,7 +12,7 @@ - + From fad0e2457a0efdb9a1ebee3eabb7ca8fc968eecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:04:36 +0000 Subject: [PATCH 009/682] Bump Microsoft.AspNetCore.Authentication.OpenIdConnect Bumps [Microsoft.AspNetCore.Authentication.OpenIdConnect](https://github.com/dotnet/aspnetcore) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authentication.OpenIdConnect dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index eab35d01..9e50d9ab 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -10,7 +10,7 @@ - + From c71f48f8aef16362d691772e9ae337083b30ed8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:32:49 +0000 Subject: [PATCH 010/682] Bump Microsoft.EntityFrameworkCore.Sqlite from 7.0.2 to 7.0.3 Bumps [Microsoft.EntityFrameworkCore.Sqlite](https://github.com/dotnet/efcore) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- .../LinkDotNet.Blog.IntegrationTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 9e50d9ab..c6f90b59 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -11,7 +11,7 @@ - + diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index 0b2ff070..6049fc16 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -7,7 +7,7 @@ - + From 8cc7542a2284255086e2380c120cedbad8f2d3dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 09:02:04 +0000 Subject: [PATCH 011/682] Bump GitInfo from 2.3.0 to 3.0.0 Bumps [GitInfo](https://github.com/devlooped/GitInfo) from 2.3.0 to 3.0.0. - [Release notes](https://github.com/devlooped/GitInfo/releases) - [Changelog](https://github.com/devlooped/GitInfo/blob/main/changelog.md) - [Commits](https://github.com/devlooped/GitInfo/compare/v2.3.0...v3.0.0) --- updated-dependencies: - dependency-name: GitInfo dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index c6f90b59..be92bd94 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -8,7 +8,7 @@ - + From 9340db54de309373018ea01a898b7577f134b1b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 09:13:57 +0000 Subject: [PATCH 012/682] Bump GitInfo from 3.0.0 to 3.0.2 Bumps [GitInfo](https://github.com/devlooped/GitInfo) from 3.0.0 to 3.0.2. - [Release notes](https://github.com/devlooped/GitInfo/releases) - [Changelog](https://github.com/devlooped/GitInfo/blob/main/changelog.md) - [Commits](https://github.com/devlooped/GitInfo/compare/v3.0.0...v3.0.2) --- updated-dependencies: - dependency-name: GitInfo dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index be92bd94..6256875d 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -8,7 +8,7 @@ - + From 2a601fc0e1d4867c16d06af95669c312f3cce4b6 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 20 Feb 2023 20:54:05 +0100 Subject: [PATCH 013/682] Update packages --- src/Directory.Build.props | 2 +- tests/Directory.Build.props | 2 +- .../Admin/Dashboard/Components/VisitCountPerPageTests.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0affc4e8..a3e428bd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index ff69fb6c..83eafa6b 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs index 3f6898b2..0ffbf9ed 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs @@ -53,7 +53,7 @@ public async Task ShouldFilterByDate() { UrlClicked = $"blogPost/{blogPost2.Id}", DateClicked = DateOnly.MinValue }; var clicked4 = new UserRecord { UrlClicked = $"blogPost/{blogPost1.Id}", DateClicked = new DateOnly(2021, 1, 1) }; - await DbContext.UserRecords.AddRangeAsync(new[] { clicked1, clicked2, clicked3, clicked4 }); + await DbContext.UserRecords.AddRangeAsync(clicked1, clicked2, clicked3, clicked4); await DbContext.SaveChangesAsync(); using var ctx = new TestContext(); ctx.ComponentFactories.Add(); @@ -88,7 +88,7 @@ public async Task ShouldShowTotalClickCount() { UrlClicked = $"blogPost/{blogPost2.Id}", DateClicked = DateOnly.MinValue }; var clicked4 = new UserRecord { UrlClicked = $"blogPost/{blogPost1.Id}", DateClicked = new DateOnly(2021, 1, 1) }; - await DbContext.UserRecords.AddRangeAsync(new[] { clicked1, clicked2, clicked3, clicked4 }); + await DbContext.UserRecords.AddRangeAsync(clicked1, clicked2, clicked3, clicked4); await DbContext.SaveChangesAsync(); using var ctx = new TestContext(); RegisterRepositories(ctx); From 51d3665efb45c82c85f19ccfdb94929d96437bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:03:43 +0000 Subject: [PATCH 014/682] Bump Blazored.Toast from 4.0.0 to 4.1.0 Bumps [Blazored.Toast](https://github.com/Blazored/Toast) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/Blazored/Toast/releases) - [Commits](https://github.com/Blazored/Toast/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: Blazored.Toast dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 6256875d..4c3d068c 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -5,7 +5,7 @@ - + From a63781a3500a031db80730c10076ceb836f86f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:22:38 +0000 Subject: [PATCH 015/682] Bump GitInfo from 3.0.2 to 3.0.4 Bumps [GitInfo](https://github.com/devlooped/GitInfo) from 3.0.2 to 3.0.4. - [Release notes](https://github.com/devlooped/GitInfo/releases) - [Changelog](https://github.com/devlooped/GitInfo/blob/main/changelog.md) - [Commits](https://github.com/devlooped/GitInfo/compare/v3.0.2...v3.0.4) --- updated-dependencies: - dependency-name: GitInfo dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 4c3d068c..5dd7c152 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -8,7 +8,7 @@ - + From 0d6d72852adcd7821c78653597940a3d4e889752 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Feb 2023 08:21:19 +0000 Subject: [PATCH 016/682] Bump Microsoft.NET.Test.Sdk from 17.5.0-preview-20221221-03 to 17.5.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.5.0-preview-20221221-03 to 17.5.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.5.0-preview-20221221-03...v17.5.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 83eafa6b..c3600149 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -4,7 +4,7 @@ - + From 67d392b6476a4dd816de0791d8b6a1329ecae5b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:25:13 +0000 Subject: [PATCH 017/682] Bump bunit from 1.16.2 to 1.17.2 Bumps [bunit](https://github.com/bUnit-dev/bUnit) from 1.16.2 to 1.17.2. - [Release notes](https://github.com/bUnit-dev/bUnit/releases) - [Changelog](https://github.com/bUnit-dev/bUnit/blob/main/CHANGELOG.md) - [Commits](https://github.com/bUnit-dev/bUnit/compare/v1.16.2...v1.17.2) --- updated-dependencies: - dependency-name: bunit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index c3600149..e299cfaa 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,7 @@ - + From b7e4183272c3f16670289b9d87b0c20ea016e4a4 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Feb 2023 13:29:33 +0100 Subject: [PATCH 018/682] Use Submit on Form instead of button click to fix test --- .../Web/Features/AboutMe/Components/TalksTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/AboutMe/Components/TalksTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/AboutMe/Components/TalksTests.cs index ead41aa5..8a00ba80 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/AboutMe/Components/TalksTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/AboutMe/Components/TalksTests.cs @@ -28,7 +28,7 @@ public void WhenAddingTalkThenDisplayedToUser() cut.Find("#talk-date").Change(new DateTime(2022, 10, 2)); cut.Find("#talk-content").Input("text"); - cut.Find("#talk-submit").Click(); + cut.Find("form").Submit(); cut.WaitForState(() => cut.HasComponent()); var entry = cut.FindComponent(); From 8ef31bfb8d674a4910099edd58a0728bf4ed4a20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:08:47 +0000 Subject: [PATCH 019/682] Bump Markdig from 0.30.4 to 0.31.0 Bumps [Markdig](https://github.com/xoofx/markdig) from 0.30.4 to 0.31.0. - [Release notes](https://github.com/xoofx/markdig/releases) - [Changelog](https://github.com/xoofx/markdig/blob/master/changelog.md) - [Commits](https://github.com/xoofx/markdig/compare/0.30.4...0.31.0) --- updated-dependencies: - dependency-name: Markdig dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 5dd7c152..8c3fe132 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -9,7 +9,7 @@ - + From d3901e583350b4ebcfc199009da161989493b4e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:08:31 +0000 Subject: [PATCH 020/682] Bump bunit from 1.17.2 to 1.18.4 Bumps [bunit](https://github.com/bUnit-dev/bUnit) from 1.17.2 to 1.18.4. - [Release notes](https://github.com/bUnit-dev/bUnit/releases) - [Changelog](https://github.com/bUnit-dev/bUnit/blob/main/CHANGELOG.md) - [Commits](https://github.com/bUnit-dev/bUnit/compare/v1.17.2...v1.18.4) --- updated-dependencies: - dependency-name: bunit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index e299cfaa..6aa1d631 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,7 @@ - + From 06c5c97af0fe951216da6261ee66873d0b703508 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 09:04:25 +0000 Subject: [PATCH 021/682] Bump SonarAnalyzer.CSharp from 8.53.0.62665 to 8.54.0.64047 Bumps [SonarAnalyzer.CSharp](https://github.com/SonarSource/sonar-dotnet) from 8.53.0.62665 to 8.54.0.64047. - [Release notes](https://github.com/SonarSource/sonar-dotnet/releases) - [Commits](https://github.com/SonarSource/sonar-dotnet/compare/8.53.0.62665...8.54.0.64047) --- updated-dependencies: - dependency-name: SonarAnalyzer.CSharp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/Directory.Build.props | 2 +- tests/Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a3e428bd..9cdb6739 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6aa1d631..33da0494 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From e7996bd26c1a29a4cf45e97fc65c4142f490b29b Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 9 Mar 2023 20:42:46 +0100 Subject: [PATCH 022/682] Don't allow multi-save --- .../Admin/BlogPostEditor/Components/CreateNewBlogPost.razor | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 2614e4e8..be925c1f 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -62,7 +62,7 @@ otherwise its original date } - +
@@ -99,6 +99,8 @@ private CreateNewModel model = new(); + private bool canSubmit = true; + protected override void OnParametersSet() { if (BlogPost == null) @@ -120,8 +122,10 @@ private async Task OnValidBlogPostCreatedAsync() { + canSubmit = false; await OnBlogPostCreated.InvokeAsync(model.ToBlogPost()); ClearModel(); + canSubmit = true; } private void ClearModel() From efcd3c6f8a7c609cb600c597950571bd7cd2e4ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:05:21 +0000 Subject: [PATCH 023/682] Bump Microsoft.AspNetCore.Mvc.Testing from 7.0.3 to 7.0.4 Bumps [Microsoft.AspNetCore.Mvc.Testing](https://github.com/dotnet/aspnetcore) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Mvc.Testing dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.IntegrationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index 6049fc16..46475fd0 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -6,7 +6,7 @@ - + From 5218c3a2ab8d24b98bbf1baa2416e80d6aacbea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:05:29 +0000 Subject: [PATCH 024/682] Bump Microsoft.AspNetCore.Authentication.OpenIdConnect Bumps [Microsoft.AspNetCore.Authentication.OpenIdConnect](https://github.com/dotnet/aspnetcore) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md) - [Commits](https://github.com/dotnet/aspnetcore/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authentication.OpenIdConnect dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 8c3fe132..bd69dab5 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -10,7 +10,7 @@ - + From 17ddae3ec72ee7fc5a9e0b5d712fb24a3c33a422 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:31:51 +0000 Subject: [PATCH 025/682] Bump Microsoft.EntityFrameworkCore from 7.0.3 to 7.0.4 Bumps [Microsoft.EntityFrameworkCore](https://github.com/dotnet/efcore) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../LinkDotNet.Blog.Infrastructure.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj index 7b4ef5e3..911ab196 100644 --- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj +++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj @@ -7,7 +7,7 @@ - + From 1a34e2d386d7a4e91064873abff1bfce451e2235 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:31:55 +0000 Subject: [PATCH 026/682] Bump Microsoft.EntityFrameworkCore.Sqlite from 7.0.3 to 7.0.4 Bumps [Microsoft.EntityFrameworkCore.Sqlite](https://github.com/dotnet/efcore) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.Sqlite dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- .../LinkDotNet.Blog.IntegrationTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index bd69dab5..3fbf604b 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -11,7 +11,7 @@ - + diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index 46475fd0..f9d9dd12 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -7,7 +7,7 @@ - + From 9837dc3c1f8f9ac13a0ae9137de500eeaf07ccea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 11:01:35 +0000 Subject: [PATCH 027/682] Bump Microsoft.EntityFrameworkCore.SqlServer from 7.0.3 to 7.0.4 Bumps [Microsoft.EntityFrameworkCore.SqlServer](https://github.com/dotnet/efcore) from 7.0.3 to 7.0.4. - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: Microsoft.EntityFrameworkCore.SqlServer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 3fbf604b..629e3c8d 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -12,7 +12,7 @@ - + From 995342390fc04326a7dfefe2d4e0a5861b7f9b91 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 11 Mar 2023 10:32:59 +0100 Subject: [PATCH 028/682] Removing GitInfo due to sponsorlink blocking builds --- .../Features/Home/Components/AccessControl.razor | 6 ++---- src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index d24ec0df..4ed7addb 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -16,7 +16,7 @@
  • Sitemap
  • -
  • @_version
  • +
  • Releases
  • @@ -27,8 +27,6 @@ @code { - private const string _version = $"{ThisAssembly.Git.BaseTag} commit:{ThisAssembly.Git.Commit}"; - - [Parameter] + [Parameter] public string CurrentUri { get; set; } = string.Empty; } diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 629e3c8d..4c912cf4 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -8,7 +8,6 @@ - From 028ca4c2070c173509a36758f083916fdd9e4d1e Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 21 Mar 2023 17:08:04 +0100 Subject: [PATCH 029/682] refactor: simplified caching --- .../Persistence/CachedRepository.cs | 39 ++++--------------- .../Persistence/CachedRepositoryTests.cs | 7 +--- 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs index 7f27bc86..4e9902de 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs @@ -1,19 +1,16 @@ using System; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; namespace LinkDotNet.Blog.Infrastructure.Persistence; -public sealed class CachedRepository : IRepository, IDisposable +public sealed class CachedRepository : IRepository where T : Entity { private readonly IRepository repository; private readonly IMemoryCache memoryCache; - private CancellationTokenSource resetToken = new(); public CachedRepository(IRepository repository, IMemoryCache memoryCache) { @@ -21,21 +18,13 @@ public CachedRepository(IRepository repository, IMemoryCache memoryCache) this.memoryCache = memoryCache; } - private MemoryCacheEntryOptions Options => new() - { - ExpirationTokens = { new CancellationChangeToken(resetToken.Token) }, - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7), - }; - public async ValueTask GetByIdAsync(string id) { - if (!memoryCache.TryGetValue(id, out T value)) + return await memoryCache.GetOrCreateAsync(id, async entry => { - value = await repository.GetByIdAsync(id); - memoryCache.Set(id, value, Options); - } - - return value; + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7); + return await repository.GetByIdAsync(id); + }); } public async ValueTask> GetAllAsync( @@ -58,26 +47,12 @@ public async ValueTask> GetAllByProjectionAsync resetToken?.Dispose(); - - private void ResetCache() - { - if (resetToken is { IsCancellationRequested: false, Token.CanBeCanceled: true }) - { - resetToken.Cancel(); - resetToken.Dispose(); - } - - resetToken = new CancellationTokenSource(); + memoryCache.Remove(id); } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs b/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs index 39f06ab0..2f3df503 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs @@ -9,7 +9,7 @@ namespace LinkDotNet.Blog.UnitTests.Infrastructure.Persistence; -public sealed class CachedRepositoryTests : IDisposable +public sealed class CachedRepositoryTests { private readonly Mock> repositoryMock; private readonly CachedRepository sut; @@ -110,11 +110,6 @@ public async Task ShouldGetFreshDataAfterDelete() Times.Exactly(2)); } - public void Dispose() - { - sut.Dispose(); - } - private void SetupRepository() { var blogPost = new BlogPostBuilder().Build(); From 8f30a15519398de1ef3b6f1d996a5ddd57de22f0 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 21 Mar 2023 17:12:50 +0100 Subject: [PATCH 030/682] Use sliding window for cache --- .../Persistence/CachedRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs index 4e9902de..8dd04f6e 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs @@ -22,7 +22,7 @@ public async ValueTask GetByIdAsync(string id) { return await memoryCache.GetOrCreateAsync(id, async entry => { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7); + entry.SlidingExpiration = TimeSpan.FromDays(7); return await repository.GetByIdAsync(id); }); } From a4e5f7d91eea429f168c0b497477a959608df215 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:03:19 +0000 Subject: [PATCH 031/682] Bump SonarAnalyzer.CSharp from 8.54.0.64047 to 8.55.0.65544 Bumps [SonarAnalyzer.CSharp](https://github.com/SonarSource/sonar-dotnet) from 8.54.0.64047 to 8.55.0.65544. - [Release notes](https://github.com/SonarSource/sonar-dotnet/releases) - [Commits](https://github.com/SonarSource/sonar-dotnet/compare/8.54.0.64047...8.55.0.65544) --- updated-dependencies: - dependency-name: SonarAnalyzer.CSharp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/Directory.Build.props | 2 +- tests/Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9cdb6739..e38266b0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 33da0494..fd7b40af 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From e5145603b9dee2c5345bb5d9e3555bb69c4558e5 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 14:25:15 +0100 Subject: [PATCH 032/682] Submit reverse --- .../Admin/BlogPostEditor/Components/CreateNewBlogPost.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index be925c1f..ca006410 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -62,7 +62,7 @@ otherwise its original date
    } - +
    From 7fe51f7958ba415e2dd69b7afc5d629e3d2fb8e3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 17:44:31 +0100 Subject: [PATCH 033/682] Added BackgroundService and Validation --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 25 +++++++- .../Components/CreateNewModel.cs | 24 +++++++- ...hedWithScheduledDateValidationAttribute.cs | 18 ++++++ .../Features/BlogPostPublisher.cs | 58 +++++++++++++++++++ src/LinkDotNet.Blog.Web/Program.cs | 4 +- .../Web/Features/BlogPostPublisherTests.cs | 45 ++++++++++++++ .../BlogPostBuilder.cs | 18 +++++- .../Domain/BlogPostTests.cs | 23 +++++++- ...thScheduledDateValidationAttributeTests.cs | 30 ++++++++++ 9 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs create mode 100644 src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs create mode 100644 tests/LinkDotNet.Blog.IntegrationTests/Web/Features/BlogPostPublisherTests.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index 792a8b79..bbc31d5b 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -22,9 +22,11 @@ private BlogPost() public DateTime UpdatedDate { get; private set; } + public DateTime? ScheduledPublishDate { get; private set; } + public virtual ICollection Tags { get; private set; } - public bool IsPublished { get; set; } + public bool IsPublished { get; private set; } public int Likes { get; set; } @@ -35,15 +37,22 @@ public static BlogPost Create( string previewImageUrl, bool isPublished, DateTime? updatedDate = null, + DateTime? scheduledPublishDate = null, IEnumerable tags = null, string previewImageUrlFallback = null) { + if (scheduledPublishDate is not null && isPublished) + { + throw new InvalidOperationException("Can't schedule publish date if the blog post is already published."); + } + var blogPost = new BlogPost { Title = title, ShortDescription = shortDescription, Content = content, UpdatedDate = updatedDate ?? DateTime.Now, + ScheduledPublishDate = scheduledPublishDate, PreviewImageUrl = previewImageUrl, PreviewImageUrlFallback = previewImageUrlFallback, IsPublished = isPublished, @@ -53,6 +62,17 @@ public static BlogPost Create( return blogPost; } + public void Publish() + { + if (ScheduledPublishDate is not null) + { + UpdatedDate = ScheduledPublishDate.Value; + ScheduledPublishDate = null; + } + + IsPublished = true; + } + public void Update(BlogPost from) { if (from == this) @@ -64,6 +84,7 @@ public void Update(BlogPost from) ShortDescription = from.ShortDescription; Content = from.Content; UpdatedDate = from.UpdatedDate; + ScheduledPublishDate = from.ScheduledPublishDate; PreviewImageUrl = from.PreviewImageUrl; PreviewImageUrlFallback = from.PreviewImageUrlFallback; IsPublished = from.IsPublished; @@ -85,4 +106,4 @@ private void ReplaceTags(IEnumerable tags) } } } -} \ No newline at end of file +} diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index 75bb3a36..e65d6445 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -17,6 +17,7 @@ public class CreateNewModel private bool shouldUpdateDate; private string tags; private string previewImageUrlFallback; + private DateTime? scheduledPublishDate; [Required] [MaxLength(256)] @@ -65,6 +66,7 @@ public string PreviewImageUrl } [Required] + [PublishedWithScheduledDateValidation] public bool IsPublished { get => isPublished; @@ -86,6 +88,16 @@ public bool ShouldUpdateDate } } + public DateTime? ScheduledPublishDate + { + get => scheduledPublishDate; + set + { + scheduledPublishDate = value; + IsDirty = true; + } + } + public string Tags { get => tags; @@ -123,6 +135,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost) PreviewImageUrl = blogPost.PreviewImageUrl, originalUpdatedDate = blogPost.UpdatedDate, PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback, + ScheduledPublishDate = blogPost.ScheduledPublishDate, IsDirty = false, }; } @@ -134,7 +147,16 @@ public BlogPost ToBlogPost() ? null : originalUpdatedDate; - var blogPost = BlogPost.Create(Title, ShortDescription, Content, PreviewImageUrl, IsPublished, updatedDate, tagList, PreviewImageUrlFallback); + var blogPost = BlogPost.Create( + Title, + ShortDescription, + Content, + PreviewImageUrl, + IsPublished, + updatedDate, + scheduledPublishDate, + tagList, + PreviewImageUrlFallback); blogPost.Id = id; return blogPost; } diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs new file mode 100644 index 00000000..f616652e --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class PublishedWithScheduledDateValidationAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (validationContext.ObjectInstance is CreateNewModel { IsPublished: true, ScheduledPublishDate: { } }) + { + return new ValidationResult("Cannot have a scheduled publish date when the post is already published."); + } + + return ValidationResult.Success; + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs new file mode 100644 index 00000000..06ea3ec7 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LinkDotNet.Blog.Web.Features; + +public class BlogPostPublisher : BackgroundService +{ + private readonly IServiceProvider serviceProvider; + private readonly ILogger logger; + + public BlogPostPublisher(IServiceProvider serviceProvider, ILogger logger) + { + this.serviceProvider = serviceProvider; + this.logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // await Task.Yield(); + logger.LogInformation("BlogPostPublisher is starting."); + + using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); + + while (!stoppingToken.IsCancellationRequested) + { + await PublishScheduledBlogPosts(); + + await timer.WaitForNextTickAsync(stoppingToken); + } + + logger.LogInformation("BlogPostPublisher is stopping."); + } + + private async Task PublishScheduledBlogPosts() + { + logger.LogInformation("Checking for scheduled blog posts."); + + using var scope = serviceProvider.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService>(); + + var now = DateTime.UtcNow; + var scheduledBlogPosts = await repository.GetAllAsync( + filter: b => b.ScheduledPublishDate != null && b.ScheduledPublishDate <= now); + + foreach (var blogPost in scheduledBlogPosts) + { + blogPost.Publish(); + await repository.StoreAsync(blogPost); + logger.LogInformation("Published blog post with ID {BlogPostId}", blogPost.Id); + } + } +} diff --git a/src/LinkDotNet.Blog.Web/Program.cs b/src/LinkDotNet.Blog.Web/Program.cs index cfb8db80..a224a4e6 100644 --- a/src/LinkDotNet.Blog.Web/Program.cs +++ b/src/LinkDotNet.Blog.Web/Program.cs @@ -1,6 +1,7 @@ using Blazored.Toast; using LinkDotNet.Blog.Web.Authentication.Auth0; using LinkDotNet.Blog.Web.Authentication.Dummy; +using LinkDotNet.Blog.Web.Features; using LinkDotNet.Blog.Web.RegistrationExtensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -34,6 +35,7 @@ private static void RegisterServices(WebApplicationBuilder builder) builder.Services.RegisterServices(); builder.Services.AddStorageProvider(builder.Configuration); builder.Services.AddResponseCompression(); + builder.Services.AddHostedService(); if (builder.Environment.IsDevelopment()) { @@ -73,4 +75,4 @@ private static void ConfigureApp(WebApplication app) app.MapFallbackToPage("/searchByTag/{tag}", "/_Host"); app.MapFallbackToPage("/search/{searchTerm}", "/_Host"); } -} \ No newline at end of file +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/BlogPostPublisherTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/BlogPostPublisherTests.cs new file mode 100644 index 00000000..be1cbae4 --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/BlogPostPublisherTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.IntegrationTests; +using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace LinkDotNet.Blog.UnitTests.Web.Features; + +public sealed class BlogPostPublisherTests : SqlDatabaseTestBase, IDisposable +{ + private readonly BlogPostPublisher sut; + + public BlogPostPublisherTests() + { + var serviceProvider = new ServiceCollection() + .AddScoped(_ => Repository) + .BuildServiceProvider(); + + sut = new BlogPostPublisher(serviceProvider, Mock.Of>()); + } + + [Fact] + public async Task ShouldPublishScheduledBlogPosts() + { + var now = DateTime.UtcNow; + var bp1 = new BlogPostBuilder().WithScheduledPublishDate(now.AddHours(-2)).IsPublished(false).Build(); + var bp2 = new BlogPostBuilder().WithScheduledPublishDate(now.AddHours(-1)).IsPublished(false).Build(); + var bp3 = new BlogPostBuilder().WithScheduledPublishDate(now.AddHours(1)).IsPublished(false).Build(); + await Repository.StoreAsync(bp1); + await Repository.StoreAsync(bp2); + await Repository.StoreAsync(bp3); + + await sut.StartAsync(CancellationToken.None); + + (await Repository.GetByIdAsync(bp1.Id)).IsPublished.Should().BeTrue(); + (await Repository.GetByIdAsync(bp2.Id)).IsPublished.Should().BeTrue(); + (await Repository.GetByIdAsync(bp3.Id)).IsPublished.Should().BeFalse(); + } + + public void Dispose() => sut?.Dispose(); +} diff --git a/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs b/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs index 913b0068..18532c57 100644 --- a/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs +++ b/tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs @@ -14,6 +14,7 @@ public class BlogPostBuilder private string[] tags; private int likes; private DateTime? updateDate; + private DateTime? scheduledPublishDate; public BlogPostBuilder WithTitle(string title) { @@ -69,9 +70,24 @@ public BlogPostBuilder WithUpdatedDate(DateTime updateDate) return this; } + public BlogPostBuilder WithScheduledPublishDate(DateTime scheduledPublishDate) + { + this.scheduledPublishDate = scheduledPublishDate; + return this; + } + public BlogPost Build() { - var blogPost = BlogPost.Create(title, shortDescription, content, previewImageUrl, isPublished, updateDate, tags, previewImageUrlFallback); + var blogPost = BlogPost.Create( + title, + shortDescription, + content, + previewImageUrl, + isPublished, + updateDate, + scheduledPublishDate, + tags, + previewImageUrlFallback); blogPost.Likes = likes; return blogPost; } diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index 8cef2e2a..a851f29e 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -69,4 +69,25 @@ public void ShouldNotDeleteTagsWhenSameReference() bp.Tags.Should().HaveCount(1); bp.Tags.Single().Content.Should().Be("tag 1"); } -} \ No newline at end of file + + [Fact] + public void ShouldPublishBlogPost() + { + var date = new DateTime(2023, 3, 24); + var bp = new BlogPostBuilder().IsPublished(false).WithScheduledPublishDate(date).Build(); + + bp.Publish(); + + bp.IsPublished.Should().BeTrue(); + bp.ScheduledPublishDate.Should().BeNull(); + bp.UpdatedDate.Should().Be(date); + } + + [Fact] + public void ShouldThrowErrorWhenCreatingBlogPostThatIsPublishedAndHasScheduledPublishDate() + { + Action action = () => BlogPost.Create("1", "2", "3", "4", true, scheduledPublishDate: new DateTime(2023, 3, 24)); + + action.Should().Throw(); + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs new file mode 100644 index 00000000..d6a6acd2 --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.BlogPostEditor.Components; + +public class PublishedWithScheduledDateValidationAttributeTests +{ + [Fact] + public void GivenBlogPostIsPublishedAndHasScheduledDate_WhenValidating_ThenError() + { + var model = new CreateNewModel + { + Title = "Title", + ShortDescription = "Desc", + Content = "Content", + IsPublished = true, + ScheduledPublishDate = DateTime.Now, + PreviewImageUrl = "https://steven-giesel.com", + }; + var validationContext = new ValidationContext(model); + var results = new List(); + + var result = Validator.TryValidateObject(model, validationContext, results, true); + + result.Should().BeFalse(); + results.Count.Should().Be(1); + } +} From 3b270d8dcaa391a3868d9acc1ab8288f8d70e8b9 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 17:46:21 +0100 Subject: [PATCH 034/682] Remove Task.Yield --- src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs | 1 - .../Web/Features/BlogPostPublisherTests.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs index 06ea3ec7..d7642db3 100644 --- a/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs +++ b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs @@ -22,7 +22,6 @@ public BlogPostPublisher(IServiceProvider serviceProvider, ILogger, IDisposable { From 3df31b8c9beae42feb8a2bd6c0523e0723b800c2 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 17:51:34 +0100 Subject: [PATCH 035/682] ScheduledDate should be in the future --- .../Components/CreateNewModel.cs | 1 + .../FallbackUrlValidationAttribute.cs | 2 +- .../FutureDateValidationAttribute.cs | 20 +++++++++++++ .../FutureDateValidationAttributeTests.cs | 30 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttribute.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttributeTests.cs diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index e65d6445..cf5941c9 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -88,6 +88,7 @@ public bool ShouldUpdateDate } } + [FutureDateValidation] public DateTime? ScheduledPublishDate { get => scheduledPublishDate; diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FallbackUrlValidationAttribute.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FallbackUrlValidationAttribute.cs index 364ca3be..3203f3d5 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FallbackUrlValidationAttribute.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FallbackUrlValidationAttribute.cs @@ -4,7 +4,7 @@ namespace LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; [AttributeUsage(AttributeTargets.Property)] -public class FallbackUrlValidationAttribute : ValidationAttribute +public sealed class FallbackUrlValidationAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttribute.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttribute.cs new file mode 100644 index 00000000..14cebecb --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class FutureDateValidationAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value is null) + { + return ValidationResult.Success; + } + + return (DateTime)value <= DateTime.UtcNow + ? new ValidationResult("The scheduled publish date must be in the future.") + : ValidationResult.Success; + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttributeTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttributeTests.cs new file mode 100644 index 00000000..a9259f4d --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/FutureDateValidationAttributeTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.BlogPostEditor.Components; + +public class FutureDateValidationAttributeTests +{ + [Fact] + public void GivenScheduledDateIsInThePast_WhenValidating_ThenError() + { + var model = new CreateNewModel + { + Title = "Title", + ShortDescription = "Desc", + Content = "Content", + IsPublished = false, + ScheduledPublishDate = DateTime.Now.AddDays(-1), + PreviewImageUrl = "https://steven-giesel.com", + }; + var validationContext = new ValidationContext(model); + var results = new List(); + + var result = Validator.TryValidateObject(model, validationContext, results, true); + + result.Should().BeFalse(); + results.Count.Should().Be(1); + } +} From addd3020d5750e1af870658e209071f574f6f615 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 17:54:25 +0100 Subject: [PATCH 036/682] Added test for update and schedule date --- .../LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index a851f29e..6d74906e 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -90,4 +90,15 @@ public void ShouldThrowErrorWhenCreatingBlogPostThatIsPublishedAndHasScheduledPu action.Should().Throw(); } + + [Fact] + public void ShouldUpdateScheduledPublishDate() + { + var blogPost = new BlogPostBuilder().Build(); + var bp = new BlogPostBuilder().IsPublished(false).WithScheduledPublishDate(new DateTime(2023, 3, 24)).Build(); + + blogPost.Update(bp); + + blogPost.ScheduledPublishDate.Should().Be(new DateTime(2023, 3, 24)); + } } From 325b9a2b6f8e59c5da5641ffbdfad6e89497d9ec Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 18:07:43 +0100 Subject: [PATCH 037/682] Simplify logic --- .../PublishedWithScheduledDateValidationAttribute.cs | 9 +++------ ...PublishedWithScheduledDateValidationAttributeTests.cs | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs index f616652e..0dbe0c6e 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs @@ -8,11 +8,8 @@ public sealed class PublishedWithScheduledDateValidationAttribute : ValidationAt { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (validationContext.ObjectInstance is CreateNewModel { IsPublished: true, ScheduledPublishDate: { } }) - { - return new ValidationResult("Cannot have a scheduled publish date when the post is already published."); - } - - return ValidationResult.Success; + return validationContext.ObjectInstance is CreateNewModel { IsPublished: true, ScheduledPublishDate: { } } + ? new ValidationResult("Cannot have a scheduled publish date when the post is already published.") + : ValidationResult.Success; } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs index d6a6acd2..0f28f673 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttributeTests.cs @@ -16,7 +16,7 @@ public void GivenBlogPostIsPublishedAndHasScheduledDate_WhenValidating_ThenError ShortDescription = "Desc", Content = "Content", IsPublished = true, - ScheduledPublishDate = DateTime.Now, + ScheduledPublishDate = DateTime.MaxValue, PreviewImageUrl = "https://steven-giesel.com", }; var validationContext = new ValidationContext(model); From bcd881d338e281f5c669a9d637899722d3d56f37 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 19:00:13 +0100 Subject: [PATCH 038/682] Allow user to enter schedule date --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 11 ++--- .../Components/CreateNewBlogPost.razor | 38 ++++++++++++------ ...hedWithScheduledDateValidationAttribute.cs | 2 +- .../Domain/BlogPostTests.cs | 10 +++++ .../Components/CreateNewBlogPostTests.cs | 40 ++++++++++++++++++- 5 files changed, 80 insertions(+), 21 deletions(-) diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index bbc31d5b..bb03f1bc 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -46,12 +46,14 @@ public static BlogPost Create( throw new InvalidOperationException("Can't schedule publish date if the blog post is already published."); } + var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.Now; + var blogPost = new BlogPost { Title = title, ShortDescription = shortDescription, Content = content, - UpdatedDate = updatedDate ?? DateTime.Now, + UpdatedDate = blogPostUpdateDate, ScheduledPublishDate = scheduledPublishDate, PreviewImageUrl = previewImageUrl, PreviewImageUrlFallback = previewImageUrlFallback, @@ -64,12 +66,7 @@ public static BlogPost Create( public void Publish() { - if (ScheduledPublishDate is not null) - { - UpdatedDate = ScheduledPublishDate.Value; - ScheduledPublishDate = null; - } - + ScheduledPublishDate = null; IsPublished = true; } diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index ca006410..6581baf6 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -35,31 +35,43 @@ The primary image which will be used. - +
    - - - Optional: Used as a fallback if the preview image can't be used by the browser. -
    For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.
    - + + + Optional: Used as a fallback if the preview image can't be used by the browser. +
    For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.
    + +
    +
    + + + If set the blog post will be published at the given date. + A blog post with a schedule date can't be set to published. +
    - +
    - If this blog post is only draft uncheck the box + If this blog post is only draft or it will be scheduled, uncheck the box. +
    - - + +
    - @if (BlogPost != null) + @if (BlogPost != null && !IsScheduled) {

    If set the publish date is set to now, - otherwise its original date + otherwise its original date.
    } @@ -101,6 +113,8 @@ private bool canSubmit = true; + private bool IsScheduled => model.ScheduledPublishDate.HasValue; + protected override void OnParametersSet() { if (BlogPost == null) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs index 0dbe0c6e..0684051a 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/PublishedWithScheduledDateValidationAttribute.cs @@ -9,7 +9,7 @@ public sealed class PublishedWithScheduledDateValidationAttribute : ValidationAt protected override ValidationResult IsValid(object value, ValidationContext validationContext) { return validationContext.ObjectInstance is CreateNewModel { IsPublished: true, ScheduledPublishDate: { } } - ? new ValidationResult("Cannot have a scheduled publish date when the post is already published.") + ? new ValidationResult("Cannot publish the post right away and schedule it for later.") : ValidationResult.Success; } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs index 6d74906e..66696d70 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs @@ -101,4 +101,14 @@ public void ShouldUpdateScheduledPublishDate() blogPost.ScheduledPublishDate.Should().Be(new DateTime(2023, 3, 24)); } + + [Fact] + public void GivenScheduledPublishDate_WhenCreating_ThenUpdateDateIsScheduledPublishDate() + { + var date = new DateTime(2023, 3, 24); + + var bp = BlogPost.Create("1", "2", "3", "4", false, scheduledPublishDate: date); + + bp.UpdatedDate.Should().Be(date); + } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 24ac38f0..83038e92 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using AngleSharp.Html.Dom; +using AngleSharpWrappers; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components; @@ -207,4 +209,40 @@ public void ShouldNotBlogNavigationOnInitialLoad() fakeNavigationManager.History.Count.Should().Be(1); fakeNavigationManager.History.Single().State.Should().Be(NavigationState.Succeeded); } -} \ No newline at end of file + + [Fact] + public void GivenBlogPostWithSchedule_ShouldSetSchedule() + { + BlogPost blogPost = null; + var cut = RenderComponent( + p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); + cut.Find("#title").Input("My Title"); + cut.Find("#short").Input("My short Description"); + cut.Find("#content").Input("My content"); + cut.Find("#preview").Change("My preview url"); + cut.Find("#published").Change(false); + cut.Find("#scheduled").Change("01/01/2099 00:00"); + + cut.Find("form").Submit(); + + blogPost.ScheduledPublishDate.Should().Be(new DateTime(2099, 01, 01)); + } + + [Fact] + public void GivenBlogPost_WhenEnteringScheduledDate_ThenIsPublishedSetToFalse() + { + BlogPost blogPost = null; + var cut = RenderComponent( + p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); + cut.Find("#title").Input("My Title"); + cut.Find("#short").Input("My short Description"); + cut.Find("#content").Input("My content"); + cut.Find("#preview").Change("My preview url"); + cut.Find("#published").Change(true); + + cut.Find("#scheduled").Change("01/01/2099 00:00"); + + var element = cut.Find("#published").Unwrap() as IHtmlInputElement; + element.IsChecked.Should().BeFalse(); + } +} From 70d732746d3845255e2077b70c13d55779ef26a4 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 24 Mar 2023 19:06:50 +0100 Subject: [PATCH 039/682] More information to console --- src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs index d7642db3..b58b7e0f 100644 --- a/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs +++ b/src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs @@ -43,10 +43,12 @@ private async Task PublishScheduledBlogPosts() using var scope = serviceProvider.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService>(); - var now = DateTime.UtcNow; + var now = DateTime.Now; var scheduledBlogPosts = await repository.GetAllAsync( filter: b => b.ScheduledPublishDate != null && b.ScheduledPublishDate <= now); + logger.LogInformation("Found {Count} scheduled blog posts.", scheduledBlogPosts.Count); + foreach (var blogPost in scheduledBlogPosts) { blogPost.Publish(); From 092f73091f034131924e61851a86aad2568706f2 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 25 Mar 2023 10:59:15 +0100 Subject: [PATCH 040/682] Added indication for drafts and scheduled posts --- src/LinkDotNet.Blog.Domain/BlogPost.cs | 2 ++ .../Features/Components/ShortBlogPost.razor | 8 ++++++ .../Components/ShortBlogPost.razor.css | 8 ++++++ .../Properties/launchSettings.json | 4 +-- .../Domain/BlogPostTests.cs | 10 +++++++ .../Features/Components/ShortBlogPostTests.cs | 26 ++++++++++++++++++- 6 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index bb03f1bc..94c80043 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -30,6 +30,8 @@ private BlogPost() public int Likes { get; set; } + public bool IsScheduled => ScheduledPublishDate is not null; + public static BlogPost Create( string title, string shortDescription, diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index f6d6c0c7..b2408fd5 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -15,6 +15,14 @@ LazyLoadImage="@LazyLoadPreviewImage"> @code { private string LinkedInShare => $"https://www.linkedin.com/shareArticle?mini=true&url={NavigationManager.Uri}"; private string XShare => $"https://twitter.com/intent/tweet?url={NavigationManager.Uri}"; + private string BlueskyShare => $"https://bsky.app/intent/compose?text={NavigationManager.Uri}"; } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/ShareBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/ShareBlogPostTests.cs index efbdbddc..38b17095 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/ShareBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/ShareBlogPostTests.cs @@ -43,4 +43,15 @@ public void ShouldShareToX() var linkedInShare = (IHtmlAnchorElement)cut.Find("#share-x"); linkedInShare.Href.ShouldBe("https://twitter.com/intent/tweet?url=http://localhost/blogPost/1"); } + + [Fact] + public void ShouldShareToBluesky() + { + Services.GetRequiredService().NavigateTo("blogPost/1"); + + var cut = Render(); + + var blueskyShare = (IHtmlAnchorElement)cut.Find("#share-bluesky"); + blueskyShare.Href.ShouldBe("https://bsky.app/intent/compose?text=http://localhost/blogPost/1"); + } } \ No newline at end of file From 46f67e934f88cc0352a2f9f6e0cc5b9b20e47bb6 Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:31:59 -0800 Subject: [PATCH 551/682] Update minute read text From: 10 min To: 10 minute read --- src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index 2a257d38..ed5e67a6 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -29,7 +29,7 @@ } -
  • @BlogPost.ReadingTimeInMinutes min
  • +
  • @BlogPost.ReadingTimeInMinutes minute read
  • From d0dee6b222f9aad3fd6c0d4da3233a2716be27c0 Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:33:11 -0800 Subject: [PATCH 552/682] Add read time icon --- .../Features/ShowBlogPost/ShowBlogPostPage.razor.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css index a52a0558..8d4442b4 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css @@ -23,6 +23,12 @@ object-fit: cover; } +.read-time:before { + font-family: 'icons'; + font-weight: 900; + content: "\e94f"; +} + @media only screen and (max-width: 700px) { .blog-outer-box .blog-container { width: 90%; From 941de6896acbc5ee33ca18957a5481cf6bd8ff75 Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:34:04 -0800 Subject: [PATCH 553/682] Add read time to blog post Added read time to top of blog post --- .../Features/ShowBlogPost/ShowBlogPostPage.razor | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index 9af73b20..4c9a44f9 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -53,6 +53,8 @@ else if (BlogPost is not null) @BlogPost.UpdatedDate.ToShortDateString()
    + + @BlogPost.ReadingTimeInMinutes minute read @if (BlogPost.Tags is not null && BlogPost.Tags.Any()) {
    From 8ce6f210cc60436e14b550b4473948cb81d38532 Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:44:44 -0800 Subject: [PATCH 554/682] Update tag text --- .../Admin/BlogPostEditor/Components/CreateNewBlogPost.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index dbce8a86..3599dc77 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -78,7 +78,7 @@
    - +
    @if (BlogPost is not null && !IsScheduled) { From 078b293562cd8fff77d4e670a16f2b0a9036e911 Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:40:43 -0800 Subject: [PATCH 555/682] Update editor button background color --- src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index 8d7c4e40..f6934543 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -143,6 +143,10 @@ code { color: red; } +.editor-toolbar button.active, .editor-toolbar button:hover { + background: rgb(206, 206, 206, 0.5) !important; +} + #blazor-error-ui { background: lightyellow; bottom: 0; From 211d4386462bd8d2bb8620a9f67d4df8a6e7966f Mon Sep 17 00:00:00 2001 From: digitaldirk <22691956+digitaldirk@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:45:49 -0800 Subject: [PATCH 556/682] Add comment --- src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index f6934543..05b30bd4 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -143,6 +143,7 @@ code { color: red; } +/* Fixes white background/white icon for markdown editors */ .editor-toolbar button.active, .editor-toolbar button:hover { background: rgb(206, 206, 206, 0.5) !important; } From 1363283fe5d508a251528a3fa4e241672ed3af51 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 22 Nov 2024 13:28:47 +0100 Subject: [PATCH 557/682] docs: Youtube video --- Readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Readme.md b/Readme.md index af757444..a2584876 100644 --- a/Readme.md +++ b/Readme.md @@ -40,3 +40,6 @@ Thanks to all [contributors](https://github.com/linkdotnet/Blog/graphs/contribut Supporters + +## Resources +You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/)has a YouTube video/series: [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). \ No newline at end of file From 5674f084c41288faa816ceb2927f214ec1edde49 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 22 Nov 2024 14:42:10 +0100 Subject: [PATCH 558/682] fix: Whitespace (Thanks Mac!) --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index a2584876..17bb7f7e 100644 --- a/Readme.md +++ b/Readme.md @@ -41,5 +41,5 @@ Thanks to all [contributors](https://github.com/linkdotnet/Blog/graphs/contribut Supporters -## Resources +## Resources You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/)has a YouTube video/series: [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). \ No newline at end of file From 2ca3f8c156a189ceb486fa7d65b015d8595694cd Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 22 Nov 2024 14:42:29 +0100 Subject: [PATCH 559/682] And another whitespace --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 17bb7f7e..acbc3f69 100644 --- a/Readme.md +++ b/Readme.md @@ -42,4 +42,4 @@ Thanks to all [contributors](https://github.com/linkdotnet/Blog/graphs/contribut ## Resources -You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/)has a YouTube video/series: [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). \ No newline at end of file +You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/) has a YouTube video/series: [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). \ No newline at end of file From 43d3416ce8e09c7dd6b9bcaf222f189fa1ec7e73 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 16 Nov 2024 15:04:07 +0100 Subject: [PATCH 560/682] chore: Upgrade NCronJob --- Directory.Packages.props | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b8cee9c1..dbab4f21 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,10 +7,10 @@
    - - - - + + + + @@ -19,12 +19,12 @@ - - + + - + @@ -41,9 +41,8 @@ - - - - - -
    + + + + +
    \ No newline at end of file From 9e65a35e227041e0994be8a3a02ef9ccea957ad4 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 22 Nov 2024 20:27:42 +0100 Subject: [PATCH 561/682] Update Generator --- .../LinkDotNet.Blog.CriticalCSS/Generator.cs | 98 ++++++++++++++----- tools/LinkDotNet.Blog.CriticalCSS/Program.cs | 5 +- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs b/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs index 0333c13b..9357d0f3 100644 --- a/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs +++ b/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs @@ -26,35 +26,85 @@ public static async Task GenerateAsync(IReadOnlyCollectionurls) var usedCss = await page.EvaluateAsync( """ - () => { - const styleSheets = Array.from(document.styleSheets); - const usedRules = new Set(); + async () => { + const styleSheets = Array.from(document.styleSheets); + const usedRules = new Set(); + const processedUrls = new Set(); - const viewportHeight = window.innerHeight; - const elements = document.querySelectorAll('*'); - const aboveFold = Array.from(elements).filter(el => { - const rect = el.getBoundingClientRect(); - return rect.top < viewportHeight; - }); + const viewportHeight = window.innerHeight; + const elements = document.querySelectorAll('*'); + const aboveFold = Array.from(elements).filter(el => { + const rect = el.getBoundingClientRect(); + return rect.top < viewportHeight; + }); - styleSheets.forEach(sheet => { - try { - Array.from(sheet.cssRules).forEach(rule => { - if (rule.type === 1) { - aboveFold.forEach(el => { - if (el.matches(rule.selectorText)) { - usedRules.add(rule.cssText); - } - }); - } - }); - } catch (e) { + async function fetchExternalStylesheet(url) { + if (processedUrls.has(url)) return; + processedUrls.add(url); + + try { + const response = await fetch(url); + const text = await response.text(); + const blob = new Blob([text], { type: 'text/css' }); + const styleSheet = new CSSStyleSheet(); + await styleSheet.replace(text); + return styleSheet; + } catch (e) { + console.error('Failed to fetch:', url, e); + return null; + } + } + + async function processStyleSheet(sheet) { + try { + if (sheet.href) { + const externalSheet = await fetchExternalStylesheet(sheet.href); + if (externalSheet) { + Array.from(externalSheet.cssRules).forEach(processRule); } - }); + } + + Array.from(sheet.cssRules).forEach(processRule); + } catch (e) { + if (sheet.href) { + console.error('CORS issue with:', sheet.href); + } + } + } - return Array.from(usedRules); + function processRule(rule) { + switch (rule.type) { + case CSSRule.STYLE_RULE: + aboveFold.forEach(el => { + try { + if (el.matches(rule.selectorText)) { + usedRules.add(rule.cssText); + } + } catch (e) {} + }); + break; + case CSSRule.MEDIA_RULE: + if (window.matchMedia(rule.conditionText).matches) { + Array.from(rule.cssRules).forEach(processRule); + } + break; + case CSSRule.IMPORT_RULE: + processStyleSheet(rule.styleSheet); + break; + case CSSRule.FONT_FACE_RULE: + case CSSRule.KEYFRAMES_RULE: + usedRules.add(rule.cssText); + break; } - """); + } + + for (const sheet of styleSheets) { + await processStyleSheet(sheet); + } + + return Array.from(usedRules); + } + """); foreach (var css in usedCss) { diff --git a/tools/LinkDotNet.Blog.CriticalCSS/Program.cs b/tools/LinkDotNet.Blog.CriticalCSS/Program.cs index bf62155c..7047762a 100644 --- a/tools/LinkDotNet.Blog.CriticalCSS/Program.cs +++ b/tools/LinkDotNet.Blog.CriticalCSS/Program.cs @@ -110,11 +110,10 @@ static void OutputToLayout(string css, string? layoutPath) const string styleTagPattern = "]*>.*?"; const string headEndTag = ""; - var newStyleTag = $""; layoutContent = Regex.IsMatch(layoutContent, styleTagPattern, RegexOptions.Singleline) - ? Regex.Replace(layoutContent, styleTagPattern, newStyleTag, RegexOptions.Singleline) - : layoutContent.Replace(headEndTag, $"{newStyleTag}\n {headEndTag}", StringComparison.OrdinalIgnoreCase); + ? Regex.Replace(layoutContent, styleTagPattern, css, RegexOptions.Singleline) + : layoutContent.Replace(headEndTag, $"{css}\n {headEndTag}", StringComparison.OrdinalIgnoreCase); File.WriteAllText(layoutPath, layoutContent); } From 5a7e04cff24e58f96412032b81eb175cf0704509 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 22 Nov 2024 20:38:58 +0100 Subject: [PATCH 562/682] docs: Sqlite --- docs/Storage/Readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Storage/Readme.md b/docs/Storage/Readme.md index f22b0e29..15afdd2e 100644 --- a/docs/Storage/Readme.md +++ b/docs/Storage/Readme.md @@ -30,3 +30,6 @@ For MySql use the following: "PersistenceProvider": "MySql" "ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" ``` + +## Considerations +For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). \ No newline at end of file From 1882a4d242f0397caee4a8b3b19878a37352646d Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 11:11:49 +0100 Subject: [PATCH 563/682] feat: add game when user navigated to 404 --- src/LinkDotNet.Blog.Web/App.razor | 11 +- .../Features/Components/ObjectNotFound.razor | 118 ++++++++++++++++++ .../ShowBlogPost/ShowBlogPostPage.razor | 9 +- 3 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor diff --git a/src/LinkDotNet.Blog.Web/App.razor b/src/LinkDotNet.Blog.Web/App.razor index b4a5691d..6b1f7d27 100644 --- a/src/LinkDotNet.Blog.Web/App.razor +++ b/src/LinkDotNet.Blog.Web/App.razor @@ -5,14 +5,9 @@ - -
    -

    404 - o((⊙﹏⊙))o

    -
    -

    I really looked hard but I couldn't find the page you are looking for.

    -

    Go back to safety

    -
    -
    + + +
    diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor new file mode 100644 index 00000000..cd855266 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor @@ -0,0 +1,118 @@ +
    +

    404 - o((⊙﹏⊙))o

    +
    +

    I really looked hard but I couldn't find the page you are looking for.

    +

    Go back to safety

    +
    +

    Play a Number Guessing Game!

    +

    Since you are here, why not play a number guessing game?

    + + @if (!isGameStarted) + { +

    Select a difficulty level to start:

    +
    + + + +
    + } + else + { +

    I'm thinking of a number between 1 and @maxNumber. Can you guess it?

    +
    + + +
    +

    Total guesses: @guessCount

    + @if (!string.IsNullOrEmpty(message)) + { +

    @message

    + @if (gameDone) + { + + } + } + } +
    + +@code { + private int targetNumber; + private int userGuess; + private int guessCount; + private int maxNumber; + private string message = string.Empty; + private bool gameDone = false; + private bool isGameStarted = false; + + private enum DifficultyLevel + { + Easy, + Medium, + Hard + } + + private void StartGame(DifficultyLevel difficulty) + { + isGameStarted = true; + gameDone = false; + guessCount = 0; + message = string.Empty; + userGuess = 0; + + switch (difficulty) + { + case DifficultyLevel.Easy: + maxNumber = 10; + break; + case DifficultyLevel.Medium: + maxNumber = 100; + break; + case DifficultyLevel.Hard: + maxNumber = 1000; + break; + } + + var random = new Random(); + targetNumber = random.Next(1, maxNumber + 1); + } + + private void CheckGuess() + { + if (gameDone || !isGameStarted) + return; + + if (userGuess < targetNumber) + { + guessCount++; + message = "Too low, try again!"; + } + else if (userGuess > targetNumber) + { + guessCount++; + message = "Too high, try again!"; + } + else + { + guessCount++; + gameDone = true; + message = "🎉 Congratulations! You guessed the number!"; + } + } + + private void ResetGame() + { + isGameStarted = false; + gameDone = false; + message = string.Empty; + userGuess = 0; + guessCount = 0; + } + + private void HandleKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter" && !gameDone && isGameStarted) + { + CheckGuess(); + } + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor index 4c9a44f9..cfddd3eb 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor @@ -21,12 +21,7 @@ } else if (!isLoading && BlogPost is null) { -
    -

    404 - o((⊙﹏⊙))o

    -
    -

    I really looked hard but I couldn't find the page you are looking for.

    -

    Go back to safety

    -
    + } else if (BlogPost is not null) { @@ -87,7 +82,7 @@ else if (BlogPost is not null) @if (SupportConfiguration.Value.ShowUnderBlogPost) { - + } @if (AppConfiguration.Value.ShowSimilarPosts) { From d4fca7cb2ca042cad15fa918f633db6d61bb2eae Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 11:19:27 +0100 Subject: [PATCH 564/682] Update Game --- .../Features/Components/ObjectNotFound.razor | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor index cd855266..b7844586 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor @@ -4,7 +4,7 @@

    I really looked hard but I couldn't find the page you are looking for.

    Go back to safety


    -

    Play a Number Guessing Game!

    +

    Play a Number Guessing Game

    Since you are here, why not play a number guessing game?

    @if (!isGameStarted) @@ -19,6 +19,7 @@ else {

    I'm thinking of a number between 1 and @maxNumber. Can you guess it?

    +

    You have @remainingGuesses guesses left.

    @@ -26,10 +27,12 @@

    Total guesses: @guessCount

    @if (!string.IsNullOrEmpty(message)) { -

    @message

    + @if (gameDone) { - + } } } @@ -40,7 +43,10 @@ private int userGuess; private int guessCount; private int maxNumber; + private int maxGuesses; + private int remainingGuesses; private string message = string.Empty; + private string alertClass = ""; private bool gameDone = false; private bool isGameStarted = false; @@ -63,17 +69,22 @@ { case DifficultyLevel.Easy: maxNumber = 10; + maxGuesses = 3; break; case DifficultyLevel.Medium: maxNumber = 100; + maxGuesses = 7; break; case DifficultyLevel.Hard: maxNumber = 1000; + maxGuesses = 10; break; + default: + throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null); } - var random = new Random(); - targetNumber = random.Next(1, maxNumber + 1); + remainingGuesses = maxGuesses; + targetNumber = Random.Shared.Next(1, maxNumber + 1); } private void CheckGuess() @@ -81,21 +92,56 @@ if (gameDone || !isGameStarted) return; - if (userGuess < targetNumber) + if (userGuess < 1 || userGuess > maxNumber) { - guessCount++; - message = "Too low, try again!"; + message = $"Please enter a number between 1 and {maxNumber}."; + alertClass = "alert-warning"; + return; } - else if (userGuess > targetNumber) + + guessCount++; + remainingGuesses--; + + int difference = Math.Abs(userGuess - targetNumber); + double proximity = (double)difference / maxNumber; + + if (userGuess == targetNumber) { - guessCount++; - message = "Too high, try again!"; + gameDone = true; + message = "🎉 Congratulations! You guessed the number!"; + alertClass = "alert-success"; } - else + else if (remainingGuesses == 0) { - guessCount++; gameDone = true; - message = "🎉 Congratulations! You guessed the number!"; + message = $"😞 Game over! You've run out of guesses. The number was {targetNumber}."; + alertClass = "alert-danger"; + } + else + { + switch (proximity) + { + case < 0.05: + message = "🔥 Scalding Hot!"; + alertClass = "alert-danger"; + break; + case < 0.1: + message = "🌡️ Very Hot!"; + alertClass = "alert-warning"; + break; + case < 0.2: + message = "🌞 Warm."; + alertClass = "alert-info"; + break; + case < 0.3: + message = "🌤️ Cool."; + alertClass = "alert-secondary"; + break; + default: + message = "❄️ Cold!"; + alertClass = "alert-secondary"; + break; + } } } @@ -104,8 +150,10 @@ isGameStarted = false; gameDone = false; message = string.Empty; + alertClass = ""; userGuess = 0; guessCount = 0; + remainingGuesses = 0; } private void HandleKeyPress(KeyboardEventArgs e) From de2133c9fc3516e7edfeacdd7948eb25f89a5672 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 16:24:09 +0100 Subject: [PATCH 565/682] Don't warn for Random.Shared --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 6c0b3806..8bc3a182 100644 --- a/.editorconfig +++ b/.editorconfig @@ -446,6 +446,7 @@ dotnet_diagnostic.CA1056.severity = none # CA1056: Uri properties should not be dotnet_diagnostic.CA1812.severity = none # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA2201.severity = suggestion # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2227.severity = suggestion # CA2227: Collection properties should be read only +dotnet_diagnostic.CA5394.severity = none # CA5394 : Random is an insecure random number generator. # SonarAnalyzer.CSharp # https://rules.sonarsource.com/csharp From 20cc3ccd4a61ac806e1722bd6ed0f7974ec2f9c4 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 17:56:39 +0100 Subject: [PATCH 566/682] Exclude razor files as well --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 8bc3a182..e9dfe987 100644 --- a/.editorconfig +++ b/.editorconfig @@ -466,6 +466,8 @@ dotnet_diagnostic.BL0005.severity = none dotnet_diagnostic.BL0006.severity = none dotnet_diagnostic.BL0007.severity = none +dotnet_diagnostic.CA5394.severity = none # CA5394 : Random is an insecure random number generator. + ########################################## # Custom Test Code Analyzers Rules ########################################## From 2c5a1b7278810ff8692edad65f9c14fd7876b87c Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 18:07:57 +0100 Subject: [PATCH 567/682] Use RNG instead --- .editorconfig | 3 --- .../Features/Components/ObjectNotFound.razor | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index e9dfe987..6c0b3806 100644 --- a/.editorconfig +++ b/.editorconfig @@ -446,7 +446,6 @@ dotnet_diagnostic.CA1056.severity = none # CA1056: Uri properties should not be dotnet_diagnostic.CA1812.severity = none # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA2201.severity = suggestion # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2227.severity = suggestion # CA2227: Collection properties should be read only -dotnet_diagnostic.CA5394.severity = none # CA5394 : Random is an insecure random number generator. # SonarAnalyzer.CSharp # https://rules.sonarsource.com/csharp @@ -466,8 +465,6 @@ dotnet_diagnostic.BL0005.severity = none dotnet_diagnostic.BL0006.severity = none dotnet_diagnostic.BL0007.severity = none -dotnet_diagnostic.CA5394.severity = none # CA5394 : Random is an insecure random number generator. - ########################################## # Custom Test Code Analyzers Rules ########################################## diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor index b7844586..69f42f39 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ObjectNotFound.razor @@ -1,3 +1,4 @@ +@using System.Security.Cryptography

    404 - o((⊙﹏⊙))o


    @@ -84,7 +85,7 @@ } remainingGuesses = maxGuesses; - targetNumber = Random.Shared.Next(1, maxNumber + 1); + targetNumber = RandomNumberGenerator.GetInt32(1, maxNumber + 1); } private void CheckGuess() From 0643939827b1110e634789e3c9281c73077faed2 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 18:09:56 +0100 Subject: [PATCH 568/682] fix: Test --- .../Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index a39c47a8..245e7009 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -115,8 +115,7 @@ public void ShouldShowErrorPageWhenBlogPostNotFound() var cut = ctx.Render(); - cut.FindAll(".blogpost-content").ShouldBeEmpty(); - cut.FindAll("#no-blog-post-error").ShouldHaveSingleItem(); + cut.HasComponent().ShouldBeTrue(); } [Fact] From da6049fd6b5200244cb9035a164c2ce9c9a6a5b9 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 24 Nov 2024 19:10:22 +0100 Subject: [PATCH 569/682] docs: Update many readmes --- docs/Comments/Disqus.md | 2 +- docs/Comments/Giscus.md | 8 ++++---- docs/Donations/Readme.md | 16 ++++++++-------- docs/Features/AdvancedFeatures.md | 17 ++++++++++++++++- docs/SEO/Readme.md | 3 +++ 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/Comments/Disqus.md b/docs/Comments/Disqus.md index 4ac4d06e..2d30d384 100644 --- a/docs/Comments/Disqus.md +++ b/docs/Comments/Disqus.md @@ -15,4 +15,4 @@ In `appsettings.json` change following | Property | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------- | | Disqus | node | Enables the comment section via disqus. If left empty the comment secion will not be shown. | -| Shortname | string | | +| Shortname | string | The shortname provided by the disqus homepage | diff --git a/docs/Comments/Giscus.md b/docs/Comments/Giscus.md index d89c3f1f..d58442d8 100644 --- a/docs/Comments/Giscus.md +++ b/docs/Comments/Giscus.md @@ -41,7 +41,7 @@ In `appsettings.json` change following | Property | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------------- | | Giscus | node | Enables the comment section via giscus. If left empty the comment secion will not be shown. | -| Repository | string | path of you github repository, example `linkdotnet/Blog` | -| RepositoryId | string | | -| Category | string | | -| CategoryId | string | | +| Repository | string | Path of you github repository, example `linkdotnet/Blog` | +| RepositoryId | string | The id provided by giscus (`data-repository-id`) | +| Category | string | The "Category" under which the comments are histed (like "Q&A", "General", "Ideas", ...) | +| CategoryId | string | The id provided by giscus (`data-category-id`) | diff --git a/docs/Donations/Readme.md b/docs/Donations/Readme.md index 93c228c1..ec198592 100644 --- a/docs/Donations/Readme.md +++ b/docs/Donations/Readme.md @@ -18,14 +18,14 @@ Enables the usage of [Patreon](https://www.patreon.com). Only pass the user name ### Configuration ```json "SupportMe": { - "KofiToken": "ABC123", - "GithubSponsorName": "your-tag-here", - "PatreonName": "your-tag-here", - "ShowUnderBlogPost": true, - "ShowUnderIntroduction": true, - "ShowInFooter": true, - "ShowSupportMePage": true, - "SupportMePageDescription": "Buy my book here: [My Blazor Book](https://google.com) or please contribute to my open-source project here: [My Awesome Repo](https://github.com) . This can be **markdown**." + "KofiToken": "ABC123", + "GithubSponsorName": "your-tag-here", + "PatreonName": "your-tag-here", + "ShowUnderBlogPost": true, + "ShowUnderIntroduction": true, + "ShowInFooter": true, + "ShowSupportMePage": true, + "SupportMePageDescription": "Buy my book here: [My Blazor Book](https://google.com) or please contribute to my open-source project here: [My Awesome Repo](https://github.com) . This can be **markdown**." } ``` diff --git a/docs/Features/AdvancedFeatures.md b/docs/Features/AdvancedFeatures.md index 2be0be2f..b98c0ea4 100644 --- a/docs/Features/AdvancedFeatures.md +++ b/docs/Features/AdvancedFeatures.md @@ -1,5 +1,20 @@ ## Advanced Features +- [Advanced Features](#advanced-features) +- [Shortcodes](#shortcodes) + - [Creating a shortcode](#creating-a-shortcode) + - [Using a shortcode](#using-a-shortcode) + - [Limitations](#limitations) +- [Critical CSS Generator](#critical-css-generator) + - [How it works](#how-it-works) + - [Options](#options) +- [Output Modes](#output-modes) + - [Console Mode](#console-mode) + - [File Mode](#file-mode) + - [Layout Mode](#layout-mode) + - [Examples](#examples) + - [Notes](#notes) + This page lists some of the more advanced or less-used features of the blog software. ## Shortcodes @@ -62,7 +77,7 @@ The output of the "critical.css" should be copied into the head of the [`_Layout ## Output Modes -### #Console Mode +### Console Mode Outputs the critical CSS directly to the console: ```sh diff --git a/docs/SEO/Readme.md b/docs/SEO/Readme.md index 0b5e8a01..d56823b8 100644 --- a/docs/SEO/Readme.md +++ b/docs/SEO/Readme.md @@ -36,5 +36,8 @@ This blog also offers an RSS feed ([RSS 2.0 specification](https://validator.w3. This blog offers to generate a [sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap) that lists all blog posts, the archive and pages of the blog. A sitemap can be generated in the Admin tab of the navigation bar under "Sitemap". This allows, especially new sites that don't have many inbound links, to be indexed easier by search engines. +## JSON LD +This blog supports a JSON-LD for structured data. The current support is limited / rudimentary. Information like `Headline` (the title of the blog post), `Author`, `PublishDated` and `PreviewImage` are present. + ## Critical CSS The blog offers an integrated tool, that generates critical CSS for the blog. Read more about it in the ["*Advanced Features*"](../Features/AdvancedFeatures.md) section. \ No newline at end of file From 0b286364a6d7e8c6807cc730b066808522766b8b Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 27 Nov 2024 18:59:41 +0000 Subject: [PATCH 570/682] fix: add jobregistration to avoid warning --- .../BackgroundServiceRegistrationExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/BackgroundServiceRegistrationExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/BackgroundServiceRegistrationExtensions.cs index 9eb3d787..a2f8eec6 100644 --- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/BackgroundServiceRegistrationExtensions.cs +++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/BackgroundServiceRegistrationExtensions.cs @@ -22,6 +22,7 @@ public static void AddBackgroundServices(this IServiceCollection services) .ExecuteWhen(s => s.RunJob()); options.AddJob(p => p.WithCronExpression("0/10 * * * *")); + options.AddJob(); }); } } From 7290b7f8e1efc45de6d43359cbf3e87248d7fb24 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 27 Nov 2024 19:32:09 +0000 Subject: [PATCH 571/682] fix some issues with the devcontainer --- .devcontainer/devcontainer.json | 8 ++++++-- src/LinkDotNet.Blog.Web/Properties/launchSettings.json | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2efc84d7..ca896493 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,8 +25,12 @@ } }, - "postCreateCommand": "dotnet dev-certs https --check --trust" - + "postCreateCommand": "dotnet dev-certs https --trust", + "portsAttributes": { + "5001": { + "protocol": "https" + } + } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } diff --git a/src/LinkDotNet.Blog.Web/Properties/launchSettings.json b/src/LinkDotNet.Blog.Web/Properties/launchSettings.json index d3406fe3..7ba5c778 100644 --- a/src/LinkDotNet.Blog.Web/Properties/launchSettings.json +++ b/src/LinkDotNet.Blog.Web/Properties/launchSettings.json @@ -21,9 +21,9 @@ }, "LinkDotNet.Blog.Web": { "commandName": "Project", - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From 6701e024aa42eb6458402fa71a5e95f69f28a6a8 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 27 Nov 2024 20:36:38 +0100 Subject: [PATCH 572/682] docs: Update readme --- Readme.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index acbc3f69..6dd69efc 100644 --- a/Readme.md +++ b/Readme.md @@ -41,5 +41,13 @@ Thanks to all [contributors](https://github.com/linkdotnet/Blog/graphs/contribut Supporters +## Try it out with **Codespaces** + +This repository offers a [GitHub Codespace](https://github.com/features/codespaces) where you can easily run and modify the Blog completely in your browser, no IDE or local .net installation needed. + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/linkdotnet/Blog) + ## Resources -You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/) has a YouTube video/series: [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). \ No newline at end of file +You want a visual walkthrough through the features and details? The awesome [@ncosentino / DevLeader](https://github.com/ncosentino/) has a YouTube video/series: + * [*"WordPress Is A DUMPSTER FIRE - Build A Blog In Blazor!"*](https://www.youtube.com/watch?v=RGq2s25xTPE). + * [*"WordPress is HISTORY! Get Your Own Blazor Blog Running TODAY!"*](https://www.youtube.com/watch?v=A2vAO7jxFz4) \ No newline at end of file From 391fd5d81d52ec811d7ff4b6004a3c46597102df Mon Sep 17 00:00:00 2001 From: Francesco Colaianni Date: Wed, 27 Nov 2024 20:50:52 +0100 Subject: [PATCH 573/682] Feat: #353 convert raw html to markdown and viceversa (#381) * feat: #353 convert raw html to markdown and viceversa * chore: refactor code with creator suggestions * fix: Move package to correct category --------- Co-authored-by: Steven Giesel --- Directory.Packages.props | 1 + .../Components/CreateNewBlogPost.razor | 259 ++++++++++-------- .../Components/FeatureInfoDialog.razor | 5 +- .../LinkDotNet.Blog.Web.csproj | 1 + .../Components/CreateNewBlogPostTests.cs | 33 ++- 5 files changed, 182 insertions(+), 117 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dbab4f21..399e41bd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,6 +25,7 @@ + diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 3599dc77..411eb1ef 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -1,7 +1,8 @@ -@using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Domain @using LinkDotNet.Blog.Infrastructure @using LinkDotNet.Blog.Infrastructure.Persistence @using LinkDotNet.Blog.Web.Features.Services +@using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components @using NCronJob @inject IJSRuntime JSRuntime @inject ICacheInvalidator CacheInvalidator @@ -9,100 +10,106 @@ @inject IRepository ShortCodeRepository
    -

    @Title

    - - -
    - - - -
    -
    - - -
    -
    - - - -
    - - -
    -
    -
    - - - The primary image which will be used. - -
    -
    - - - Optional: Used as a fallback if the preview image can't be used by the browser. -
    For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.
    - -
    -
    - - - If set the blog post will be published at the given date. - A blog post with a schedule date can't be set to published. - -
    -
    - -
    - If this blog post is only draft or it will be scheduled, uncheck the box. - -
    -
    - - -
    - @if (BlogPost is not null && !IsScheduled) - { -
    - -
    - If set the publish date is set to now, - otherwise its original date. -
    - } -
    - -
    - The first page of the blog is cached. Therefore, the blog post is not immediately visible. - Head over to settings to invalidate the cache or enable the checkmark. -
    - The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page. -
    -
    - -
    -
    -
    -
    +

    @Title

    + + +
    + + + +
    +
    + + +
    +
    + + + +
    + + +
    +
    +
    + + + The primary image which will be used. + +
    +
    + + + + Optional: Used as a fallback if the preview image can't be used by the browser. +
    For example using a jpg or png as fallback for avif which is not supported in Safari or Edge. +
    + +
    +
    + + + + If set the blog post will be published at the given date. + A blog post with a schedule date can't be set to published. + + +
    +
    + +
    + If this blog post is only draft or it will be scheduled, uncheck the box. + +
    +
    + + +
    + @if (BlogPost is not null && !IsScheduled) + { +
    + +
    + + If set the publish date is set to now, + otherwise its original date. + +
    + } +
    + +
    + The first page of the blog is cached. Therefore, the blog post is not immediately visible. + Head over to settings to invalidate the cache or enable the checkmark. +
    + The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page. +
    +
    + +
    +
    +
    +
    @@ -127,17 +134,21 @@ private CreateNewModel model = new(); - private bool canSubmit = true; - private IPagedList shortCodes = PagedList.Empty; + private string? originalContent = null; + private bool IsContentConverted => !string.IsNullOrWhiteSpace(originalContent); + private string ConvertLabel => !IsContentConverted ? "Convert to markdown" : "Restore"; - private bool IsScheduled => model.ScheduledPublishDate.HasValue; + private bool canSubmit = true; + private IPagedList shortCodes = PagedList.Empty; - protected override async Task OnInitializedAsync() - { - shortCodes = await ShortCodeRepository.GetAllAsync(); - } + private bool IsScheduled => model.ScheduledPublishDate.HasValue; + + protected override async Task OnInitializedAsync() + { + shortCodes = await ShortCodeRepository.GetAllAsync(); + } - protected override void OnParametersSet() + protected override void OnParametersSet() { if (BlogPost is null) { @@ -149,16 +160,16 @@ private async Task OnValidBlogPostCreatedAsync() { - canSubmit = false; + canSubmit = false; await OnBlogPostCreated.InvokeAsync(model.ToBlogPost()); if (model.ShouldInvalidateCache) - { - CacheInvalidator.Cancel(); - } + { + CacheInvalidator.Cancel(); + } InstantJobRegistry.RunInstantJob(parameter: true); ClearModel(); - canSubmit = true; + canSubmit = true; } private void ClearModel() @@ -186,17 +197,35 @@ private Task ReplaceShortCodes(string markdown) { - foreach (var code in shortCodes) - { - markdown = markdown.Replace($"[[{code.Name}]]", code.MarkdownContent); - } + foreach (var code in shortCodes) + { + markdown = markdown.Replace($"[[{code.Name}]]", code.MarkdownContent); + } - return Task.FromResult(MarkdownConverter.ToMarkupString(markdown).Value); + return Task.FromResult(MarkdownConverter.ToMarkupString(markdown).Value); } private void OpenShortCodeDialog() { - ShortCodeDialog.Open(); - StateHasChanged(); + ShortCodeDialog.Open(); + StateHasChanged(); + } + + /// + /// Convert content from HTML to Markdown and viceversa + /// + private void ConvertContent() + { + if (IsContentConverted) + { + model.Content = originalContent!; + originalContent = null; + } + else + { + originalContent = model.Content; + var converter = new ReverseMarkdown.Converter(); + model.Content = converter.Convert(model.Content); + } } } diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FeatureInfoDialog.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FeatureInfoDialog.razor index 73710409..299668f0 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FeatureInfoDialog.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/FeatureInfoDialog.razor @@ -1,4 +1,4 @@ - +

    Here you will find a comprehensive list over feature you can use additional to classic markdown

    Features marked with are experimental and can change heavily, get removed or the usage changes.

    @@ -12,6 +12,9 @@ <slide-show-image src="https://picsum.photos/500/200" title="Title 2"></slide-show-image> <slide-show-image src="https://picsum.photos/550/200" title="Title 3"></slide-show-image> </slide-show> +
    +

    Convert To Markdown

    +

    By clicking the button in the editor "Convert to Markdown", you will be able to transform HTML content into Markdown content. You will be also able to restore the original content

    @code { diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index 77d4d2fa..e61c8b8a 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -15,6 +15,7 @@ +
    diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs index 3969a8dc..472db955 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AngleSharp.Html.Dom; using Blazored.Toast.Services; @@ -283,4 +283,35 @@ public void GivenBlogPost_WhenCacheInvalidatedOptionIsSet_CacheIsInvalidated() token.IsCancellationRequested.ShouldBeTrue(); } + + [Fact] + public void ShouldTransformHtmlToMarkdown() + { + var cut = Render(); + var content = cut.Find("#content"); + content.Input("

    My Content

    "); + var btnConvert = cut.Find("#convert"); + btnConvert.Click(); + content.TextContent.ShouldBeEquivalentTo("### My Content"); + btnConvert.TextContent.Trim().ShouldBeEquivalentTo("Restore"); + } + + [Fact] + public void ShouldRestoreMarkdownToHtml() + { + var cut = Render(); + string htmlContent = "

    My Content

    "; + string markdownContent = "### My Content"; + var content = cut.Find("#content"); + + content.Input(htmlContent); + var btnConvert = cut.Find("#convert"); + btnConvert.Click(); + content.TextContent.ShouldBeEquivalentTo(markdownContent); + btnConvert.TextContent.Trim().ShouldBeEquivalentTo("Restore"); + + btnConvert.Click(); + content.TextContent.ShouldBeEquivalentTo(htmlContent); + btnConvert.TextContent.Trim().ShouldBeEquivalentTo("Convert to markdown"); + } } From 5de9bf53a3a3d31f1b9b6f9706d5d08136b2223a Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 18:38:50 +0100 Subject: [PATCH 574/682] Better colors --- Directory.Packages.props | 4 ++-- src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 399e41bd..a1f061b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -46,4 +46,4 @@ - \ No newline at end of file + diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index 05b30bd4..8ef71d41 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -144,8 +144,11 @@ code { } /* Fixes white background/white icon for markdown editors */ +.editor-toolbar { + background: rgb(36, 90, 170) !important; +} .editor-toolbar button.active, .editor-toolbar button:hover { - background: rgb(206, 206, 206, 0.5) !important; + background: rgb(36, 90, 170) !important; } #blazor-error-ui { From 911c8a7b4aebffbac73282ab59319a5084a67deb Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 18:40:26 +0100 Subject: [PATCH 575/682] Remove duplicate --- src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index 8ef71d41..46b1bf02 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -111,10 +111,6 @@ code { content: "\e9d5"; } -.editor-toolbar { - background: #696969; -} - .copy-btn { border-color: whitesmoke !important; } @@ -147,6 +143,7 @@ code { .editor-toolbar { background: rgb(36, 90, 170) !important; } + .editor-toolbar button.active, .editor-toolbar button:hover { background: rgb(36, 90, 170) !important; } From aed1b8f233bd88f8a14bef70410825b8178ae593 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 18:51:29 +0100 Subject: [PATCH 576/682] chore: Upgrade packages --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a1f061b0..851b8922 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,7 +31,7 @@ - + @@ -44,6 +44,6 @@ - + From 7ab3e6e2b3923e89a02d9bda1729fc5e8df5cca3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 19:00:58 +0100 Subject: [PATCH 577/682] Initial set --- Directory.Packages.props | 7 +- MIGRATION.md | 21 ++ docs/Storage/Readme.md | 19 ++ .../20241128180004_Initial.Designer.cs | 244 ++++++++++++++++++ .../Migrations/20241128180004_Initial.cs | 167 ++++++++++++ .../Migrations/BlogDbContextModelSnapshot.cs | 241 +++++++++++++++++ .../LinkDotNet.Blog.Web.csproj | 46 ++-- 7 files changed, 721 insertions(+), 24 deletions(-) create mode 100644 src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.Designer.cs create mode 100644 src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.cs create mode 100644 src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 851b8922..64101710 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,9 +8,10 @@ - - - + + + + diff --git a/MIGRATION.md b/MIGRATION.md index 7e2a366d..182f8295 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,6 +3,27 @@ This document describes the changes that need to be made to migrate from one ver ## 8.0 to 9.0 +### SQL - Entity Framework Migrations + +Starting with `v9.0` the blog uses Entity Framework Migrations for all SQL providers. If you are already having a database you need to run the following script that creates the history table and the initial entry: +```bash +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; +GO + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'20241128180004_Initial', N'8.0.11'); +GO +``` + +Read more in the [documentation](docs/Storage/Readme.md). + ### Support / Donation section If you used the sponsor/donation mechanism in the `appsettings.json` like this: ```json diff --git a/docs/Storage/Readme.md b/docs/Storage/Readme.md index 15afdd2e..74772ef2 100644 --- a/docs/Storage/Readme.md +++ b/docs/Storage/Readme.md @@ -31,5 +31,24 @@ For MySql use the following: "ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" ``` +## Entity Framework Migrations + +For the SQL providers (`SqlServer`, `Sqlite`, `MySql`), you can use Entity Framework Core Migrations to create and manage the database schema. The whole documentation can be found under [*"Entity Framework Core tools reference"*](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). The short version is that you can use the following steps: + +```bash +dotnet ef database update --project src/LinkDotNet.Blog.Infrastructure --startup-project src/LinkDotNet.Blog.Web --connection "" +``` + +The `--connection` parameter is optional - if you don't specify it, it will try to grab it from your `appsettings.json` file. +The other options is to create a sql script you can execute against your database: + +```bash +dotnet ef migrations script --project src/LinkDotNet.Blog.Infrastructure --startup-project src/LinkDotNet.Blog.Web +``` + +Here is the full documentation: [*"Applying Migrations"*](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying). + +Alternatively, the blog calls `Database.EnsureCreated()` on startup, which creates the database schema if it does not exist. So you are not forced to use migrations. + ## Considerations For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.Designer.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.Designer.cs new file mode 100644 index 00000000..75db3798 --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.Designer.cs @@ -0,0 +1,244 @@ +// +using System; +using LinkDotNet.Blog.Infrastructure.Persistence.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LinkDotNet.Blog.Infrastructure.Migrations +{ + [DbContext(typeof(BlogDbContext))] + [Migration("20241128180004_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsPublished") + .HasColumnType("INTEGER"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PreviewImageUrl") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("PreviewImageUrlFallback") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ReadingTimeInMinutes") + .HasColumnType("INTEGER"); + + b.Property("ScheduledPublishDate") + .HasColumnType("TEXT"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsPublished", "UpdatedDate") + .IsDescending(false, true) + .HasDatabaseName("IX_BlogPosts_IsPublished_UpdatedDate"); + + b.ToTable("BlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPostRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("BlogPostId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Clicks") + .HasColumnType("INTEGER"); + + b.Property("DateClicked") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BlogPostRecords"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ProfileInformationEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ProfileInformationEntries"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ShortCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("MarkdownContent") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ShortCodes"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.SimilarBlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("SimilarBlogPostIds") + .IsRequired() + .HasMaxLength(1350) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SimilarBlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Capability") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("IconUrl") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProficiencyLevel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Talk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Place") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PresentationTitle") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Talks"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.UserRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("DateClicked") + .HasColumnType("TEXT"); + + b.Property("UrlClicked") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.cs new file mode 100644 index 00000000..eebd8cac --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/20241128180004_Initial.cs @@ -0,0 +1,167 @@ +/// +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LinkDotNet.Blog.Infrastructure.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BlogPostRecords", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + BlogPostId = table.Column(type: "TEXT", maxLength: 256, nullable: false), + DateClicked = table.Column(type: "TEXT", nullable: false), + Clicks = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BlogPostRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BlogPosts", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + Title = table.Column(type: "TEXT", maxLength: 256, nullable: false), + ShortDescription = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + PreviewImageUrl = table.Column(type: "TEXT", maxLength: 1024, nullable: false), + PreviewImageUrlFallback = table.Column(type: "TEXT", maxLength: 1024, nullable: true), + UpdatedDate = table.Column(type: "TEXT", nullable: false), + ScheduledPublishDate = table.Column(type: "TEXT", nullable: true), + Tags = table.Column(type: "TEXT", maxLength: 2048, nullable: false), + IsPublished = table.Column(type: "INTEGER", nullable: false), + Likes = table.Column(type: "INTEGER", nullable: false), + ReadingTimeInMinutes = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BlogPosts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ProfileInformationEntries", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + Content = table.Column(type: "TEXT", maxLength: 512, nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProfileInformationEntries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShortCodes", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + MarkdownContent = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 512, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShortCodes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SimilarBlogPosts", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + SimilarBlogPostIds = table.Column(type: "TEXT", maxLength: 1350, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SimilarBlogPosts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Skills", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + IconUrl = table.Column(type: "TEXT", maxLength: 1024, nullable: true), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Capability = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProficiencyLevel = table.Column(type: "TEXT", maxLength: 32, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Skills", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Talks", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + PresentationTitle = table.Column(type: "TEXT", maxLength: 256, nullable: false), + Place = table.Column(type: "TEXT", maxLength: 256, nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + PublishedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Talks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserRecords", + columns: table => new + { + Id = table.Column(type: "TEXT", unicode: false, nullable: false), + DateClicked = table.Column(type: "TEXT", nullable: false), + UrlClicked = table.Column(type: "TEXT", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRecords", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_BlogPosts_IsPublished_UpdatedDate", + table: "BlogPosts", + columns: new[] { "IsPublished", "UpdatedDate" }, + descending: new[] { false, true }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BlogPostRecords"); + + migrationBuilder.DropTable( + name: "BlogPosts"); + + migrationBuilder.DropTable( + name: "ProfileInformationEntries"); + + migrationBuilder.DropTable( + name: "ShortCodes"); + + migrationBuilder.DropTable( + name: "SimilarBlogPosts"); + + migrationBuilder.DropTable( + name: "Skills"); + + migrationBuilder.DropTable( + name: "Talks"); + + migrationBuilder.DropTable( + name: "UserRecords"); + } + } +} diff --git a/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs new file mode 100644 index 00000000..d5b07c7f --- /dev/null +++ b/src/LinkDotNet.Blog.Infrastructure/Migrations/BlogDbContextModelSnapshot.cs @@ -0,0 +1,241 @@ +// +using System; +using LinkDotNet.Blog.Infrastructure.Persistence.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LinkDotNet.Blog.Infrastructure.Migrations +{ + [DbContext(typeof(BlogDbContext))] + partial class BlogDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsPublished") + .HasColumnType("INTEGER"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PreviewImageUrl") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("PreviewImageUrlFallback") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ReadingTimeInMinutes") + .HasColumnType("INTEGER"); + + b.Property("ScheduledPublishDate") + .HasColumnType("TEXT"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tags") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsPublished", "UpdatedDate") + .IsDescending(false, true) + .HasDatabaseName("IX_BlogPosts_IsPublished_UpdatedDate"); + + b.ToTable("BlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.BlogPostRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("BlogPostId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Clicks") + .HasColumnType("INTEGER"); + + b.Property("DateClicked") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BlogPostRecords"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ProfileInformationEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ProfileInformationEntries"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.ShortCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("MarkdownContent") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ShortCodes"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.SimilarBlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("SimilarBlogPostIds") + .IsRequired() + .HasMaxLength(1350) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SimilarBlogPosts"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Capability") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("IconUrl") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProficiencyLevel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.Talk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Place") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PresentationTitle") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PublishedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Talks"); + }); + + modelBuilder.Entity("LinkDotNet.Blog.Domain.UserRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("DateClicked") + .HasColumnType("TEXT"); + + b.Property("UrlClicked") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj index e61c8b8a..4625b7a6 100644 --- a/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj +++ b/src/LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj @@ -1,26 +1,30 @@ - - net9.0 - enable - + + net9.0 + enable + - - - - - - - - - - - - - + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + - - - - + + + + From f15ef7f962c57c37142382cf82f85ce6df4d0a0b Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 19:29:37 +0100 Subject: [PATCH 578/682] macOS fix --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 182f8295..0425a78b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,7 +3,7 @@ This document describes the changes that need to be made to migrate from one ver ## 8.0 to 9.0 -### SQL - Entity Framework Migrations +### SQL - Entity Framework Migrations Starting with `v9.0` the blog uses Entity Framework Migrations for all SQL providers. If you are already having a database you need to run the following script that creates the history table and the initial entry: ```bash From 36f683b90959a2911c5e10519b67e23fc0f0a0d3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 28 Nov 2024 19:39:04 +0100 Subject: [PATCH 579/682] docs: Added migration stuff --- Readme.md | 1 + docs/Migrations/Readme.md | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 docs/Migrations/Readme.md diff --git a/Readme.md b/Readme.md index 6dd69efc..7b0680c4 100644 --- a/Readme.md +++ b/Readme.md @@ -28,6 +28,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js] - [Installation Instructions](./docs/Setup/Readme.md) - Releases on [github.com](https://github.com/linkdotnet/Blog/releases) +- [Upgrading from an older version? What now?](./docs/Migrations/Readme.md) ## License diff --git a/docs/Migrations/Readme.md b/docs/Migrations/Readme.md new file mode 100644 index 00000000..5eb0637c --- /dev/null +++ b/docs/Migrations/Readme.md @@ -0,0 +1,9 @@ +# Upgrading from an older version + +The blog tries to follow “semantic versioning” as much as possible. This means that when a release comes out with a new “major” version, there are breaking changes. What is meant by “breaking changes”? This is either the database (i.e. the database schema has changed) or the “appsettings.json” file has breaking changes. The C# code is excluded. This can change completely at any time. Interfaces and classes that are used internally do not adhere to semantic versioning. Breaking changes also means that migration of some kind is mandatory. For example, because areas in appsettings.json have been completely restructured or because a new database table has been added. + +This is contrasted by Minor changes. These are things where the user does not necessarily have to intervene. In appsettings.json, for example, these could be new features that are simply not automatically switched on or off. This means that these changes are optional. + +Breaking changes are recorded in the [MIGRATION.md](../../MIGRATION.md). Since version 9 of the blog, “Entity Framework Migrations” has been introduced for all SQL providers. You can read more in the [documentation](../Storage/Readme.md). In a nutshell, this means that database migration can be carried out easily via the “ef migration” CLI tool. More on this in the documentation linked above. + +Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md. \ No newline at end of file From 28fb2ef2c334f65a65e38022b9af05a04f5ac1db Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 30 Nov 2024 09:26:21 +0100 Subject: [PATCH 580/682] fix: Don't apply title multiple times (Fixes #385) --- .../ApplicationConfiguration.cs | 2 - .../ConfigurationExtension.cs | 1 - .../Components/CreateNewBlogPost.razor | 3 +- .../Features/Home/Index.razor | 2 + .../Features/MarkdownConverter.cs | 1 - src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml | 1 - .../SmokeTests.cs | 83 ++++++++++++++----- .../Admin/ShortCodes/ShortCodesPageTests.cs | 1 - .../Components/SimilarBlogPostSectionTests.cs | 1 - .../ShowBlogPost/ShowBlogPostPageTests.cs | 4 +- .../Web/Features/SimilarBlogPostJobTests.cs | 1 - .../RavenDbRegistrationExtensionsTests.cs | 1 - .../SqliteRegistrationExtensionsTests.cs | 1 - .../StorageProviderExtensionsTests.cs | 1 - .../Shared/Admin/BlogPostAdminActionsTests.cs | 1 - .../Web/Shared/NavMenuTests.cs | 2 - .../SupportMeConfigurationBuilder.cs | 1 - .../Web/ApplicationConfigurationTests.cs | 1 - .../Features/Home/Components/FooterTests.cs | 2 - .../Home/Components/IntroductionCardTests.cs | 1 - .../Components/CommentSectionTests.cs | 1 - .../ShowBlogPost/Components/DisqusTests.cs | 1 - .../Components/DonationSectionTests.cs | 2 - .../ShowBlogPost/Components/GiscusTests.cs | 1 - .../Components/GithubSponsorTests.cs | 1 - .../ShowBlogPost/Components/KofiTests.cs | 1 - .../ShowBlogPost/Components/PatreonTests.cs | 1 - .../ShowBlogPost/ShowBlogPostPageTests.cs | 2 - 28 files changed, 64 insertions(+), 57 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs index 0bdcfd09..33274f8c 100644 --- a/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs +++ b/src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs @@ -1,5 +1,3 @@ -using System.ComponentModel.DataAnnotations; - namespace LinkDotNet.Blog.Web; public sealed record ApplicationConfiguration diff --git a/src/LinkDotNet.Blog.Web/ConfigurationExtension.cs b/src/LinkDotNet.Blog.Web/ConfigurationExtension.cs index b0be48df..0029c06f 100644 --- a/src/LinkDotNet.Blog.Web/ConfigurationExtension.cs +++ b/src/LinkDotNet.Blog.Web/ConfigurationExtension.cs @@ -6,7 +6,6 @@ using LinkDotNet.Blog.Web.Features.SupportMe.Components; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; namespace LinkDotNet.Blog.Web; diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 411eb1ef..e1f5e848 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -2,7 +2,6 @@ @using LinkDotNet.Blog.Infrastructure @using LinkDotNet.Blog.Infrastructure.Persistence @using LinkDotNet.Blog.Web.Features.Services -@using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components @using NCronJob @inject IJSRuntime JSRuntime @inject ICacheInvalidator CacheInvalidator @@ -136,7 +135,7 @@ private string? originalContent = null; private bool IsContentConverted => !string.IsNullOrWhiteSpace(originalContent); - private string ConvertLabel => !IsContentConverted ? "Convert to markdown" : "Restore"; + private string ConvertLabel => !IsContentConverted ? "Convert to markdown" : "Restore"; private bool canSubmit = true; private IPagedList shortCodes = PagedList.Empty; diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Index.razor b/src/LinkDotNet.Blog.Web/Features/Home/Index.razor index 7fdcbec2..8997b330 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Index.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Index.razor @@ -15,6 +15,8 @@ @inject IOptions AppConfiguration @inject NavigationManager NavigationManager +@AppConfiguration.Value.BlogName + diff --git a/src/LinkDotNet.Blog.Web/Features/MarkdownConverter.cs b/src/LinkDotNet.Blog.Web/Features/MarkdownConverter.cs index e689ab95..f540a42a 100644 --- a/src/LinkDotNet.Blog.Web/Features/MarkdownConverter.cs +++ b/src/LinkDotNet.Blog.Web/Features/MarkdownConverter.cs @@ -7,7 +7,6 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; using Microsoft.AspNetCore.Components; -using MongoDB.Driver.Linq; namespace LinkDotNet.Blog.Web.Features; diff --git a/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml b/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml index 2b94ec7d..6fbea811 100644 --- a/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml @@ -9,7 +9,6 @@ - @AppConfiguration.Value.BlogName diff --git a/tests/LinkDotNet.Blog.IntegrationTests/SmokeTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/SmokeTests.cs index dacacd38..3cd6a487 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/SmokeTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/SmokeTests.cs @@ -1,8 +1,16 @@ using System; +using System.Linq; +using System.Net.Http; using System.Threading.Tasks; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; using LinkDotNet.Blog.Infrastructure.Persistence; +using LinkDotNet.Blog.Infrastructure.Persistence.Sql; +using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using TestContext = Xunit.TestContext; namespace LinkDotNet.Blog.IntegrationTests; @@ -10,36 +18,25 @@ namespace LinkDotNet.Blog.IntegrationTests; public sealed class SmokeTests : IClassFixture>, IDisposable, IAsyncDisposable { private readonly WebApplicationFactory factory; + private readonly IServiceScope scope; + private readonly HttpClient client; public SmokeTests(WebApplicationFactory factory) { this.factory = factory.WithWebHostBuilder(builder => { + builder.UseSetting("BlogName", "Tests Title"); builder.UseSetting("PersistenceProvider", PersistenceProvider.Sqlite.Key); builder.UseSetting("ConnectionString", "DataSource=file::memory:?cache=shared"); }); + + scope = this.factory.Services.CreateScope(); + client = this.factory.CreateClient(); } [Fact] public async Task ShouldBootUpApplication() { - using var client = factory.CreateClient(); - - var result = await client.GetAsync("/", cancellationToken: TestContext.Current.CancellationToken); - - result.IsSuccessStatusCode.ShouldBeTrue(); - } - - [Fact] - public async Task ShouldBootUpWithSqlDatabase() - { - await using var sqlFactory = factory.WithWebHostBuilder(builder => - { - builder.UseSetting("PersistenceProvider", PersistenceProvider.Sqlite.Key); - builder.UseSetting("ConnectionString", "DataSource=file::memory:?cache=shared"); - }); - using var client = sqlFactory.CreateClient(); - var result = await client.GetAsync("/", cancellationToken: TestContext.Current.CancellationToken); result.IsSuccessStatusCode.ShouldBeTrue(); @@ -48,8 +45,6 @@ public async Task ShouldBootUpWithSqlDatabase() [Fact] public async Task ShouldAllowDotsForTagSearch() { - using var client = factory.CreateClient(); - var result = await client.GetAsync("/searchByTag/.NET5", cancellationToken: TestContext.Current.CancellationToken); result.IsSuccessStatusCode.ShouldBeTrue(); @@ -58,8 +53,6 @@ public async Task ShouldAllowDotsForTagSearch() [Fact] public async Task ShouldAllowDotsForFreeTextSearch() { - using var client = factory.CreateClient(); - var result = await client.GetAsync("/search/.NET5", cancellationToken: TestContext.Current.CancellationToken); result.IsSuccessStatusCode.ShouldBeTrue(); @@ -69,7 +62,6 @@ public async Task ShouldAllowDotsForFreeTextSearch() public async Task RssFeedShouldBeRateLimited() { const int numberOfRequests = 16; - using var client = factory.CreateClient(); for (var i = 0; i < numberOfRequests - 1; i++) { @@ -81,10 +73,55 @@ public async Task RssFeedShouldBeRateLimited() lastResult.IsSuccessStatusCode.ShouldBeFalse(); } - public void Dispose() => factory?.Dispose(); + [Fact] + public async Task ShowingBlogPost_ShouldOnlyHaveOneTitle() + { + var blogPost = new BlogPostBuilder().WithTitle("My Blog Post").Build(); + var contextProvider = scope.ServiceProvider.GetRequiredService>(); + await using var context = await contextProvider.CreateDbContextAsync(TestContext.Current.CancellationToken); + await context.BlogPosts.AddAsync(blogPost, TestContext.Current.CancellationToken); + await context.SaveChangesAsync(TestContext.Current.CancellationToken); + + var result = await client.GetAsync($"/blogPost/{blogPost.Id}", cancellationToken: TestContext.Current.CancellationToken); + + result.IsSuccessStatusCode.ShouldBeTrue(); + var content = await result.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + var document = GetHtmlDocument(content); + var titleTags = document.QuerySelectorAll("title"); + titleTags.Length.ShouldBe(1); + titleTags.Single().TextContent.ShouldBe("My Blog Post"); + } + + [Fact] + public async Task IndexPage_HasTitleFromConfiguration() + { + var result = await client.GetAsync("/", cancellationToken: TestContext.Current.CancellationToken); + + result.IsSuccessStatusCode.ShouldBeTrue(); + var content = await result.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + var document = GetHtmlDocument(content); + var titleTags = document.QuerySelectorAll("title"); + titleTags.Length.ShouldBe(1); + titleTags.Single().TextContent.ShouldBe("Tests Title"); + } + + public void Dispose() + { + scope.Dispose(); + client.Dispose(); + factory?.Dispose(); + } public async ValueTask DisposeAsync() { + scope.Dispose(); + client.Dispose(); await factory.DisposeAsync(); } + + private static IHtmlDocument GetHtmlDocument(string html) + { + var parser = new HtmlParser(); + return parser.ParseDocument(html); + } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/ShortCodes/ShortCodesPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/ShortCodes/ShortCodesPageTests.cs index 117f75c4..f05d341f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/ShortCodes/ShortCodesPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/ShortCodes/ShortCodesPageTests.cs @@ -1,6 +1,5 @@ using System.Linq; using System.Threading.Tasks; -using AngleSharp.Html.Dom; using Blazored.Toast.Services; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities.Fakes; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs index d781a73a..4bb230e1 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 245e7009..22923471 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Threading.Tasks; using Blazored.Toast.Services; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SimilarBlogPostJobTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SimilarBlogPostJobTests.cs index 04a9e07f..b6f441ae 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SimilarBlogPostJobTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SimilarBlogPostJobTests.cs @@ -3,7 +3,6 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence.Sql; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/RavenDbRegistrationExtensionsTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/RavenDbRegistrationExtensionsTests.cs index c02906a8..1a7ff65a 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/RavenDbRegistrationExtensionsTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/RavenDbRegistrationExtensionsTests.cs @@ -1,7 +1,6 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.RegistrationExtensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/SqliteRegistrationExtensionsTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/SqliteRegistrationExtensionsTests.cs index 200f683c..66daa463 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/SqliteRegistrationExtensionsTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/SqliteRegistrationExtensionsTests.cs @@ -1,7 +1,6 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.RegistrationExtensions; using Microsoft.Extensions.Options; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/StorageProviderExtensionsTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/StorageProviderExtensionsTests.cs index 901daaac..cabc1e40 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/StorageProviderExtensionsTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/RegistrationExtensions/StorageProviderExtensionsTests.cs @@ -3,7 +3,6 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.RegistrationExtensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Admin/BlogPostAdminActionsTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Admin/BlogPostAdminActionsTests.cs index dc7ed18d..c55e827f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Admin/BlogPostAdminActionsTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Admin/BlogPostAdminActionsTests.cs @@ -4,7 +4,6 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; -using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using NCronJob; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index 9d04018f..27e47dab 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -1,8 +1,6 @@ using System.Linq; using AngleSharp.Html.Dom; -using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Home.Components; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/LinkDotNet.Blog.TestUtilities/SupportMeConfigurationBuilder.cs b/tests/LinkDotNet.Blog.TestUtilities/SupportMeConfigurationBuilder.cs index 58b19cf0..4485ce11 100644 --- a/tests/LinkDotNet.Blog.TestUtilities/SupportMeConfigurationBuilder.cs +++ b/tests/LinkDotNet.Blog.TestUtilities/SupportMeConfigurationBuilder.cs @@ -1,4 +1,3 @@ -using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using LinkDotNet.Blog.Web.Features.SupportMe.Components; namespace LinkDotNet.Blog.TestUtilities; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs index 35a194b1..8005b883 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/ApplicationConfigurationTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Authentication.OpenIdConnect; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using Microsoft.Extensions.Configuration; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/FooterTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/FooterTests.cs index cd119547..1b715506 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/FooterTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/FooterTests.cs @@ -1,6 +1,4 @@ -using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.Home.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/IntroductionCardTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/IntroductionCardTests.cs index 4cb05272..6d9242fd 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/IntroductionCardTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Home/Components/IntroductionCardTests.cs @@ -1,6 +1,5 @@ using System.Linq; using AngleSharp.Css.Dom; -using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Home.Components; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/CommentSectionTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/CommentSectionTests.cs index 3acb7781..3f8ab8ca 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/CommentSectionTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/CommentSectionTests.cs @@ -1,5 +1,4 @@ using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DisqusTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DisqusTests.cs index 0df30e72..0b8ff2a6 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DisqusTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DisqusTests.cs @@ -1,6 +1,5 @@ using System.Linq; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DonationSectionTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DonationSectionTests.cs index 39363ee9..60ef7526 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DonationSectionTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/DonationSectionTests.cs @@ -1,6 +1,4 @@ using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; -using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using LinkDotNet.Blog.Web.Features.SupportMe.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GiscusTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GiscusTests.cs index 12596509..04d83c0f 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GiscusTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GiscusTests.cs @@ -1,6 +1,5 @@ using System.Linq; using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GithubSponsorTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GithubSponsorTests.cs index a894c3b0..976a91aa 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GithubSponsorTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/GithubSponsorTests.cs @@ -1,5 +1,4 @@ using AngleSharp.Html.Dom; -using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using LinkDotNet.Blog.Web.Features.SupportMe.Components; namespace LinkDotNet.Blog.UnitTests.Web.Features.ShowBlogPost.Components; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/KofiTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/KofiTests.cs index bac41444..55e9120e 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/KofiTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/KofiTests.cs @@ -1,5 +1,4 @@ using AngleSharp.Html.Dom; -using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using LinkDotNet.Blog.Web.Features.SupportMe.Components; namespace LinkDotNet.Blog.UnitTests.Web.Features.ShowBlogPost.Components; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/PatreonTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/PatreonTests.cs index e602c486..e681e690 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/PatreonTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/Components/PatreonTests.cs @@ -1,5 +1,4 @@ using AngleSharp.Html.Dom; -using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; using LinkDotNet.Blog.Web.Features.SupportMe.Components; namespace LinkDotNet.Blog.UnitTests.Web.Features.ShowBlogPost.Components; diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index a0b25b37..bc013a40 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq.Expressions; using System.Threading.Tasks; using AngleSharp.Html.Dom; using Blazored.Toast.Services; From 41215b13af46b3fbe4e151771124542dcce96a73 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 30 Nov 2024 09:28:43 +0100 Subject: [PATCH 581/682] Added other pagetitle to be HTML compliant --- src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor | 2 ++ src/LinkDotNet.Blog.Web/Features/Archive/ArchivePage.razor | 2 ++ src/LinkDotNet.Blog.Web/Features/SupportMe/SupportMePage.razor | 1 + 3 files changed, 5 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor b/src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor index a8421f44..25bc392c 100644 --- a/src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor +++ b/src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor @@ -6,6 +6,8 @@ @inject NavigationManager NavigationManager @inject IOptions ProfileInformation +About Me - @ProfileInformation.Value.Name + @if (AppConfiguration.Value.IsAboutMeEnabled) { Repository +@inject IOptions AppConfiguration +Archive @AppConfiguration.Value.BlogName
    diff --git a/src/LinkDotNet.Blog.Web/Features/SupportMe/SupportMePage.razor b/src/LinkDotNet.Blog.Web/Features/SupportMe/SupportMePage.razor index 99a970b9..7e6c8b5c 100644 --- a/src/LinkDotNet.Blog.Web/Features/SupportMe/SupportMePage.razor +++ b/src/LinkDotNet.Blog.Web/Features/SupportMe/SupportMePage.razor @@ -7,6 +7,7 @@ @if (SupportConfiguration.Value.ShowSupportMePage) { + Support Me Date: Wed, 4 Dec 2024 20:36:27 +0100 Subject: [PATCH 582/682] fix: Whole width icon (fixes #389) --- src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index 46b1bf02..3f7e5860 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -148,6 +148,12 @@ code { background: rgb(36, 90, 170) !important; } +/* Remove when https://github.com/Megabit/Blazorise/issues/5876 fixed */ +.editor-toolbar .table { + display: inline-block !important; + width: fit-content !important; +} + #blazor-error-ui { background: lightyellow; bottom: 0; From 3a973aa5cb35471b3235bc66c6a37c99cd2a09c6 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 5 Dec 2024 07:33:09 +0100 Subject: [PATCH 583/682] fix: Added PageTitle to all admin pages --- .../Admin/BlogPostEditor/Components/CreateNewBlogPost.razor | 2 ++ .../Features/Admin/BlogPostEditor/UpdateBlogPostPage.razor | 2 ++ .../Features/Admin/Dashboard/DashboardPage.razor | 2 ++ .../Features/Admin/DraftBlogPost/DraftBlogPostPage.razor | 2 ++ .../Features/Admin/Settings/SettingsPage.razor | 2 ++ .../Features/Admin/ShortCodes/ShortCodesPage.razor | 2 ++ .../Features/Admin/Sitemap/SitemapPage.razor | 3 +++ 7 files changed, 15 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index e1f5e848..7e8c1c4f 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -8,6 +8,8 @@ @inject IInstantJobRegistry InstantJobRegistry @inject IRepository ShortCodeRepository +Creating new Blog Post +

    @Title

    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/UpdateBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/UpdateBlogPostPage.razor index 132f2d32..46dff63b 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/UpdateBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/UpdateBlogPostPage.razor @@ -6,6 +6,8 @@ @inject IRepository BlogPostRepository @inject IToastService ToastService +Updating: @blogPostFromDb?.Title + @if (blogPostFromDb is not null) { Dashboard +

    Dashboard

    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/DraftBlogPost/DraftBlogPostPage.razor b/src/LinkDotNet.Blog.Web/Features/Admin/DraftBlogPost/DraftBlogPostPage.razor index 229495f2..469a28db 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/DraftBlogPost/DraftBlogPostPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/DraftBlogPost/DraftBlogPostPage.razor @@ -5,6 +5,8 @@ @attribute [Authorize] @inject IRepository BlogPostRepository +Drafts +

    Draft Blog Posts

    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/Settings/SettingsPage.razor b/src/LinkDotNet.Blog.Web/Features/Admin/Settings/SettingsPage.razor index 4a2c48e4..cb0b9246 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/Settings/SettingsPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/Settings/SettingsPage.razor @@ -7,6 +7,8 @@ @inject IInstantJobRegistry InstantJobRegistry @attribute [Authorize] +Settings +

    Settings

    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/ShortCodes/ShortCodesPage.razor b/src/LinkDotNet.Blog.Web/Features/Admin/ShortCodes/ShortCodesPage.razor index 98e4b78e..55d91552 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/ShortCodes/ShortCodesPage.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/ShortCodes/ShortCodesPage.razor @@ -6,6 +6,8 @@ @inject IRepository ShortCodeRepository @inject IToastService ToastService +Shortcodes +

    Shortcodes

    - - - - - - - - @foreach (var url in sitemapUrlSet.Urls) - { - - - - - } - -
    UrlLast Changed
    @url.Location@url.LastModified
    - } -
    -
    - -@code { - private SitemapUrlSet? sitemapUrlSet; - private bool isGenerating; - - private async Task CreateSitemap() - { - isGenerating = true; - sitemapUrlSet = await SitemapService.CreateSitemapAsync(); - isGenerating = false; - await SitemapService.SaveSitemapToFileAsync(sitemapUrlSet); - } -} diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor index 87760dd2..83e4e2e2 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor @@ -16,7 +16,6 @@
  • Shortcodes
  • -
  • Sitemap
  • Releases
  • diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index 93aff1e8..6432c7d0 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -20,7 +20,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddSingleton(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs index c35bcaa2..2049ecfb 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs @@ -1,59 +1,41 @@ -using System; +using System; using System.IO; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; +using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; using Microsoft.AspNetCore.Components; using TestContext = Xunit.TestContext; namespace LinkDotNet.Blog.IntegrationTests.Web.Shared.Services; -public sealed class SitemapServiceTests : IDisposable +public sealed class SitemapServiceTests : SqlDatabaseTestBase { - private const string OutputDirectory = "wwwroot"; - private const string OutputFilename = $"{OutputDirectory}/sitemap.xml"; private readonly SitemapService sut; public SitemapServiceTests() - { - var repositoryMock = Substitute.For>(); - sut = new SitemapService(repositoryMock, Substitute.For(), new XmlFileWriter()); - Directory.CreateDirectory("wwwroot"); - } + => sut = new SitemapService(Repository); [Fact] public async Task ShouldSaveSitemapUrlInCorrectFormat() { - var urlSet = new SitemapUrlSet - { - Urls = - [ - new SitemapUrl { Location = "here", } - ], - }; - await sut.SaveSitemapToFileAsync(urlSet); - - var lines = await File.ReadAllTextAsync(OutputFilename, TestContext.Current.CancellationToken); - lines.ShouldBe( - @" - - - here - -"); - } - - public void Dispose() - { - if (File.Exists(OutputFilename)) - { - File.Delete(OutputFilename); - } - - if (Directory.Exists(OutputDirectory)) - { - Directory.Delete(OutputDirectory, true); - } + var publishedBlogPost = new BlogPostBuilder() + .WithTitle("Title 1") + .WithUpdatedDate(new DateTime(2024, 12, 24)) + .IsPublished() + .Build(); + var unpublishedBlogPost = new BlogPostBuilder() + .IsPublished(false) + .Build(); + await Repository.StoreAsync(publishedBlogPost); + await Repository.StoreAsync(unpublishedBlogPost); + + var sitemap = await sut.CreateSitemapAsync("https://www.linkdotnet.blog"); + + sitemap.Urls.Count.ShouldBe(3); + sitemap.Urls.ShouldContain(u => u.Location == "https://www.linkdotnet.blog/"); + sitemap.Urls.ShouldContain(u => u.Location == "https://www.linkdotnet.blog/archive"); + sitemap.Urls.ShouldContain(u => u.Location == "https://www.linkdotnet.blog/blogPost/" + publishedBlogPost.Id); } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/SitemapServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/SitemapServiceTests.cs deleted file mode 100644 index c83c524f..00000000 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/SitemapServiceTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Threading.Tasks; -using LinkDotNet.Blog.Domain; -using LinkDotNet.Blog.Infrastructure; -using LinkDotNet.Blog.Infrastructure.Persistence; -using LinkDotNet.Blog.TestUtilities; -using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; - -namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.Sitemap.Services; - -public class SitemapServiceTests : BunitContext -{ - private readonly IRepository repositoryMock; - private readonly IXmlFileWriter xmlFileWriterMock; - private readonly SitemapService sut; - private readonly BunitNavigationManager fakeNavigationManager; - - public SitemapServiceTests() - { - repositoryMock = Substitute.For>(); - fakeNavigationManager = new BunitNavigationManager(this); - - xmlFileWriterMock = Substitute.For(); - sut = new SitemapService( - repositoryMock, - fakeNavigationManager, - xmlFileWriterMock); - } - - [Fact] - public async Task ShouldCreateSitemap() - { - var bp1 = new BlogPostBuilder() - .WithUpdatedDate(new DateTime(2020, 1, 1)) - .WithTags("tag1", "tag2") - .Build(); - bp1.Id = "id1"; - var bp2 = new BlogPostBuilder() - .WithUpdatedDate(new DateTime(2019, 1, 1)) - .WithTags("tag2") - .Build(); - bp2.Id = "id2"; - repositoryMock.GetAllAsync( - Arg.Any>>(), - Arg.Any>>(), - true, - Arg.Any(), - Arg.Any()) - .Returns(new PagedList([bp1, bp2], 2, 1, 10)); - - var sitemap = await sut.CreateSitemapAsync(); - - sitemap.Urls.Count.ShouldBe(6); - sitemap.Urls[0].Location.ShouldBe($"{fakeNavigationManager.BaseUri}"); - sitemap.Urls[1].Location.ShouldBe($"{fakeNavigationManager.BaseUri}archive"); - sitemap.Urls[2].Location.ShouldBe($"{fakeNavigationManager.BaseUri}blogPost/id1"); - sitemap.Urls[3].Location.ShouldBe($"{fakeNavigationManager.BaseUri}blogPost/id2"); - sitemap.Urls[4].Location.ShouldBe($"{fakeNavigationManager.BaseUri}searchByTag/tag1"); - sitemap.Urls[5].Location.ShouldBe($"{fakeNavigationManager.BaseUri}searchByTag/tag2"); - } - - [Fact] - public async Task ShouldSaveSitemapToFile() - { - var sitemap = new SitemapUrlSet(); - - await sut.SaveSitemapToFileAsync(sitemap); - - await xmlFileWriterMock.Received(1).WriteObjectToXmlFileAsync(sitemap, "wwwroot/sitemap.xml"); - } -} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlFileWriterTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlFileWriterTests.cs deleted file mode 100644 index ffbbabfa..00000000 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlFileWriterTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; -using TestContext = Xunit.TestContext; - -namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.Sitemap.Services; - -public sealed class XmlFileWriterTests : IDisposable -{ - private const string OutputFilename = "somefile.txt"; - - [Fact] - public async Task ShouldWriteToFile() - { - var myObj = new MyObject { Property = "Prop" }; - - await new XmlFileWriter().WriteObjectToXmlFileAsync(myObj, OutputFilename); - - var content = await File.ReadAllTextAsync(OutputFilename, cancellationToken: TestContext.Current.CancellationToken); - content.ShouldNotBeNull(); - content.ShouldContain("Prop"); - } - - public void Dispose() - { - if (File.Exists(OutputFilename)) - { - File.Delete(OutputFilename); - } - } - - public class MyObject - { - public required string Property { get; set; } - } -} \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlWriterTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlWriterTests.cs new file mode 100644 index 00000000..83255470 --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/Services/XmlWriterTests.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Threading.Tasks; +using System.Xml.Serialization; +using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.Sitemap.Services; + +public sealed class XmlWriterTests +{ + [Fact] + public async Task ShouldWriteToFile() + { + var myObj = new MyObject { Property = "Prop" }; + + var buffer = await new XmlWriter().WriteToBuffer(myObj); + + var serializer = new XmlSerializer(typeof(MyObject)); + await using var memoryStream = new MemoryStream(buffer); + var myObject = serializer.Deserialize(memoryStream) as MyObject; + myObject.ShouldNotBeNull(); + } + + public class MyObject + { + public required string Property { get; set; } + } +} \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/SitemapPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/SitemapPageTests.cs deleted file mode 100644 index 4b47ffd3..00000000 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/Sitemap/SitemapPageTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using LinkDotNet.Blog.Web.Features.Admin.Sitemap; -using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; -using LinkDotNet.Blog.Web.Features.Components; -using Microsoft.Extensions.DependencyInjection; -using TestContext = Xunit.TestContext; - -namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.Sitemap; - -public class SitemapPageTests : BunitContext -{ - [Fact] - public async Task ShouldSaveSitemap() - { - AddAuthorization().SetAuthorized("steven"); - var sitemapMock = Substitute.For(); - Services.AddScoped(_ => sitemapMock); - var sitemap = new SitemapUrlSet(); - sitemapMock.CreateSitemapAsync().Returns(sitemap); - var cut = Render(); - - cut.Find("button").Click(); - - await sitemapMock.Received(1).SaveSitemapToFileAsync(sitemap); - } - - [Fact] - public void ShouldDisplaySitemap() - { - AddAuthorization().SetAuthorized("steven"); - var sitemapMock = Substitute.For(); - Services.AddScoped(_ => sitemapMock); - var sitemap = new SitemapUrlSet - { - Urls = new List - { - new() { Location = "loc", LastModified = "Now" }, - }, - }; - sitemapMock.CreateSitemapAsync().Returns(sitemap); - var cut = Render(); - - cut.Find("button").Click(); - - var row = cut.WaitForElements("tr").Last(); - row.Children.First().InnerHtml.ShouldBe("loc"); - row.Children.Last().InnerHtml.ShouldBe("Now"); - } - - [Fact] - public void ShouldShowLoadingWhenGenerating() - { - AddAuthorization().SetAuthorized("steven"); - var sitemapMock = Substitute.For(); - Services.AddScoped(_ => sitemapMock); - var sitemap = new SitemapUrlSet - { - Urls = new List - { - new() { Location = "loc", LastModified = "Now" }, - }, - }; - sitemapMock.CreateSitemapAsync().Returns(Task.Run(async () => - { - await Task.Delay(1000, TestContext.Current.CancellationToken); - return sitemap; - })); - - var cut = Render(); - - cut.Find("button").Click(); - - cut.FindComponents().Count.ShouldBe(1); - var btn = cut.Find("button"); - btn.Attributes.Any(a => a.Name == "disabled").ShouldBeTrue(); - } -} \ No newline at end of file From f0a1492f615b41ecca2ee02d604a6d7831b43f6e Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 28 Dec 2024 13:32:03 +0100 Subject: [PATCH 593/682] fix: IDE warning --- .../Features/Services/FileUpload/AzureBlobStorageService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LinkDotNet.Blog.Web/Features/Services/FileUpload/AzureBlobStorageService.cs b/src/LinkDotNet.Blog.Web/Features/Services/FileUpload/AzureBlobStorageService.cs index 0d69ff6f..e442659a 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/FileUpload/AzureBlobStorageService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/FileUpload/AzureBlobStorageService.cs @@ -47,7 +47,9 @@ private static (string rootContainer, string subContainer) SplitContainerName(st var containerNames = containerName.Split('/', StringSplitOptions.RemoveEmptyEntries); if (containerNames.Length == 0) + { return (string.Empty, string.Empty); + } var rootContainer = containerNames[0]; var subContainer = string.Join("/", containerNames.Skip(1)); From 704d0c2ecd3f1b808c07dd13ba6df9590e6ead37 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 28 Dec 2024 13:32:38 +0100 Subject: [PATCH 594/682] refactor: Remove unused usings --- src/LinkDotNet.Blog.Web/Controller/SitemapController.cs | 3 --- .../Features/Admin/Sitemap/Services/SitemapService.cs | 3 --- .../Web/Shared/Services/SitemapServiceTests.cs | 4 ---- 3 files changed, 10 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs b/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs index a213cf56..03397a33 100644 --- a/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs +++ b/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs @@ -1,8 +1,5 @@ using System; -using System.IO; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Serialization; using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs b/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs index 3b49e46c..005fcea1 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/Services/SitemapService.cs @@ -2,13 +2,10 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; -using System.IO; using System.Linq; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.Infrastructure.Persistence; -using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.Caching.Memory; namespace LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs index 2049ecfb..7fc15ca9 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/Services/SitemapServiceTests.cs @@ -1,12 +1,8 @@ using System; -using System.IO; using System.Threading.Tasks; using LinkDotNet.Blog.Domain; -using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services; -using Microsoft.AspNetCore.Components; -using TestContext = Xunit.TestContext; namespace LinkDotNet.Blog.IntegrationTests.Web.Shared.Services; From d32ca8f7c72ba6bffe9e7f629f48aff7f4fd8c13 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 30 Dec 2024 10:35:52 +0100 Subject: [PATCH 595/682] chore: Upgrade NCronJob to latest stable --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2e54c046..10254860 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + From 85053035a585b285b6e36bc39ec35cf72c71047f Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 30 Dec 2024 11:41:16 +0100 Subject: [PATCH 596/682] fix: ThemeToggler for mobile --- src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor index 420bf3c5..27cf67bb 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor @@ -54,7 +54,7 @@ - +
  • From 5d369ffd4ac15ca2e365afa682b8ae35ebc6f6d7 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 30 Dec 2024 11:47:25 +0100 Subject: [PATCH 597/682] refactor: Remove unsused objects --- .../Features/Components/MarkdownTextArea.razor | 1 - .../Features/Components/SelectionRange.cs | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 src/LinkDotNet.Blog.Web/Features/Components/SelectionRange.cs diff --git a/src/LinkDotNet.Blog.Web/Features/Components/MarkdownTextArea.razor b/src/LinkDotNet.Blog.Web/Features/Components/MarkdownTextArea.razor index 3aad691d..016f4eb9 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/MarkdownTextArea.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/MarkdownTextArea.razor @@ -87,5 +87,4 @@ ToastService.ShowError($"Error while uploading file: {e.Message}"); } } - } diff --git a/src/LinkDotNet.Blog.Web/Features/Components/SelectionRange.cs b/src/LinkDotNet.Blog.Web/Features/Components/SelectionRange.cs deleted file mode 100644 index 70ccc2dc..00000000 --- a/src/LinkDotNet.Blog.Web/Features/Components/SelectionRange.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LinkDotNet.Blog.Web.Features.Components; - -public readonly record struct SelectionRange -{ - public int Start { get; init; } - - public int End { get; init; } -} From b71e078468c82ab71dc67409fa4aa4e3c3d5d027 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 31 Dec 2024 11:21:02 +0100 Subject: [PATCH 598/682] fix: Media queries --- .../LinkDotNet.Blog.CriticalCSS/Generator.cs | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs b/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs index 3b1a45b7..6cf809ee 100644 --- a/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs +++ b/tools/LinkDotNet.Blog.CriticalCSS/Generator.cs @@ -26,11 +26,12 @@ public static async Task GenerateAsync(IReadOnlyCollection urls) await page.SetViewportSizeAsync(viewport.Width, viewport.Height); var usedCss = await page.EvaluateAsync( - """ + """ async () => { const styleSheets = Array.from(document.styleSheets); const usedRules = new Set(); const processedUrls = new Set(); + const mediaQueryRules = new Set(); const viewportHeight = window.innerHeight; const elements = document.querySelectorAll('*'); @@ -39,40 +40,6 @@ public static async Task GenerateAsync(IReadOnlyCollection urls) return rect.top < viewportHeight; }); - async function fetchExternalStylesheet(url) { - if (processedUrls.has(url)) return; - processedUrls.add(url); - - try { - const response = await fetch(url); - const text = await response.text(); - const blob = new Blob([text], { type: 'text/css' }); - const styleSheet = new CSSStyleSheet(); - await styleSheet.replace(text); - return styleSheet; - } catch (e) { - console.error('Failed to fetch:', url, e); - return null; - } - } - - async function processStyleSheet(sheet) { - try { - if (sheet.href) { - const externalSheet = await fetchExternalStylesheet(sheet.href); - if (externalSheet) { - Array.from(externalSheet.cssRules).forEach(processRule); - } - } - - Array.from(sheet.cssRules).forEach(processRule); - } catch (e) { - if (sheet.href) { - console.error('CORS issue with:', sheet.href); - } - } - } - function processRule(rule) { switch (rule.type) { case CSSRule.STYLE_RULE: @@ -85,9 +52,8 @@ function processRule(rule) { }); break; case CSSRule.MEDIA_RULE: - if (window.matchMedia(rule.conditionText).matches) { - Array.from(rule.cssRules).forEach(processRule); - } + // Always include the complete media query block + mediaQueryRules.add(rule.cssText); break; case CSSRule.IMPORT_RULE: processStyleSheet(rule.styleSheet); @@ -99,11 +65,45 @@ function processRule(rule) { } } + async function processStyleSheet(sheet) { + try { + if (sheet.href) { + const externalSheet = await fetchExternalStylesheet(sheet.href); + if (externalSheet) { + Array.from(externalSheet.cssRules).forEach(processRule); + } + } + Array.from(sheet.cssRules).forEach(processRule); + } catch (e) { + if (sheet.href) { + console.error('CORS issue with:', sheet.href); + } + } + } + + async function fetchExternalStylesheet(url) { + if (processedUrls.has(url)) return; + processedUrls.add(url); + + try { + const response = await fetch(url); + const text = await response.text(); + const blob = new Blob([text], { type: 'text/css' }); + const styleSheet = new CSSStyleSheet(); + await styleSheet.replace(text); + return styleSheet; + } catch (e) { + console.error('Failed to fetch:', url, e); + return null; + } + } + for (const sheet of styleSheets) { await processStyleSheet(sheet); } - return Array.from(usedRules); + // Combine regular rules and media queries + return [...Array.from(usedRules), ...Array.from(mediaQueryRules)]; } """); From f74ba86564170a5baaf8d726af376b809098317f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 08:18:57 +0000 Subject: [PATCH 599/682] chore(deps): bump coverlet.msbuild from 6.0.2 to 6.0.3 Bumps [coverlet.msbuild](https://github.com/coverlet-coverage/coverlet) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/coverlet-coverage/coverlet/releases) - [Commits](https://github.com/coverlet-coverage/coverlet/compare/v6.0.2...v6.0.3) --- updated-dependencies: - dependency-name: coverlet.msbuild dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 10254860..00c14ea9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,7 +39,7 @@ - + From 7702642aefe3c809151c67fc492cbbc5c72d440f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 08:16:59 +0000 Subject: [PATCH 600/682] chore(deps): bump Blazorise.Markdown and Microsoft.Extensions.Options Bumps [Blazorise.Markdown](https://github.com/Megabit/Blazorise) and [Microsoft.Extensions.Options](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Blazorise.Markdown` from 1.7.1 to 1.7.2 - [Release notes](https://github.com/Megabit/Blazorise/releases) - [Commits](https://github.com/Megabit/Blazorise/commits) Updates `Microsoft.Extensions.Options` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v9.0.0...v9.0.0) --- updated-dependencies: - dependency-name: Blazorise.Markdown dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Microsoft.Extensions.Options dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 00c14ea9..05d75499 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From cf03890e61caff208e725633ff2955bbc28cfb53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 17:48:57 +0000 Subject: [PATCH 601/682] chore(deps): bump coverlet.collector from 6.0.2 to 6.0.3 Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/coverlet-coverage/coverlet/releases) - [Commits](https://github.com/coverlet-coverage/coverlet/compare/v6.0.2...v6.0.3) --- updated-dependencies: - dependency-name: coverlet.collector dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 05d75499..578e3482 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,7 +38,7 @@ - + From d0c2182a08aa72849f26e9df0bdffab70a15f26d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 17:48:57 +0000 Subject: [PATCH 602/682] chore(deps): bump Blazorise.Bootstrap5 and Microsoft.Extensions.Options Bumps [Blazorise.Bootstrap5](https://github.com/Megabit/Blazorise) and [Microsoft.Extensions.Options](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Blazorise.Bootstrap5` from 1.7.1 to 1.7.2 - [Release notes](https://github.com/Megabit/Blazorise/releases) - [Commits](https://github.com/Megabit/Blazorise/commits) Updates `Microsoft.Extensions.Options` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v9.0.0...v9.0.0) --- updated-dependencies: - dependency-name: Blazorise.Bootstrap5 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: Microsoft.Extensions.Options dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 578e3482..07e94c29 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,7 @@ - + From ddb4ee330109b61bd90736f54698888956df27b5 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 12 Jan 2025 16:27:45 +0100 Subject: [PATCH 603/682] chore: Upgrade packages --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 07e94c29..b57f67c9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -22,10 +22,10 @@ - + - + @@ -40,8 +40,8 @@ - - + + From f034a70aa51748da4c9cedbb17070a158567140d Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 12 Jan 2025 16:34:06 +0100 Subject: [PATCH 604/682] fix: SA errors --- .../AboutMe/Components/Skill/AddSkillDialog.razor | 4 ++-- .../BlogPostEditor/Components/CreateNewBlogPost.razor | 2 -- .../Admin/Dashboard/Components/DateRangeSelector.razor | 9 +++++---- .../Features/Home/Components/SocialAccounts.razor | 2 +- .../ShowBlogPost/Components/StructuredData.razor | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/AboutMe/Components/Skill/AddSkillDialog.razor b/src/LinkDotNet.Blog.Web/Features/AboutMe/Components/Skill/AddSkillDialog.razor index e44ad6a7..fba32587 100644 --- a/src/LinkDotNet.Blog.Web/Features/AboutMe/Components/Skill/AddSkillDialog.razor +++ b/src/LinkDotNet.Blog.Web/Features/AboutMe/Components/Skill/AddSkillDialog.razor @@ -21,9 +21,9 @@
    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 7e8c1c4f..e028410f 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -181,8 +181,6 @@ } } - private void SetContentFromFile(string content) => model.Content = content; - private async Task PreventNavigationWhenDirty(LocationChangingContext context) { if (!model.IsDirty) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/Dashboard/Components/DateRangeSelector.razor b/src/LinkDotNet.Blog.Web/Features/Admin/Dashboard/Components/DateRangeSelector.razor index f8d6e73b..4424aa08 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/Dashboard/Components/DateRangeSelector.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/Dashboard/Components/DateRangeSelector.razor @@ -1,4 +1,5 @@ -
    +@using System.Globalization +
    @@ -12,19 +13,19 @@ [Parameter] public EventCallback FilterChanged { get; set; } - private Filter filter = new(); + private readonly Filter filter = new(); private async Task ApplyFilters() => await FilterChanged.InvokeAsync(filter); private async Task StartDateChanged(ChangeEventArgs args) { - filter.StartDate = args.Value is string { Length: >0 } dateTime ? DateOnly.Parse(dateTime) : null; + filter.StartDate = args.Value is string { Length: >0 } dateTime ? DateOnly.Parse(dateTime, CultureInfo.CurrentCulture) : null; await ApplyFilters(); } private async Task EndDateChanged(ChangeEventArgs args) { - filter.EndDate = args.Value is string { Length: >0 } dateTime ? DateOnly.Parse(dateTime) : null; + filter.EndDate = args.Value is string { Length: >0 } dateTime ? DateOnly.Parse(dateTime, CultureInfo.CurrentCulture) : null; await ApplyFilters(); } } diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/SocialAccounts.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/SocialAccounts.razor index 36b9862e..9675ef8c 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/SocialAccounts.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/SocialAccounts.razor @@ -22,5 +22,5 @@ [Parameter, EditorRequired] public required Social Social { get; set; } - private record SocialAccount(string Id, bool HasAccount, string? Url, string Name, string Icon); + private sealed record SocialAccount(string Id, bool HasAccount, string? Url, string Name, string Icon); } diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/StructuredData.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/StructuredData.razor index 668ff044..bfedb10c 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/StructuredData.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/StructuredData.razor @@ -46,7 +46,7 @@ await base.OnParametersSetAsync(); } - private class NewsArticle + private sealed class NewsArticle { [JsonPropertyName("@context")] public required string Context { get; set; } From 47af7587c16c25b53f0fee13667b4e488f4604e7 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 15 Jan 2025 16:08:39 +0100 Subject: [PATCH 605/682] chore: Upgrade to service release --- Directory.Packages.props | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b57f67c9..8ba0c812 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,11 +8,11 @@ - - - - - + + + + + @@ -23,15 +23,15 @@ - - + + - + - - + + @@ -47,4 +47,4 @@ - \ No newline at end of file + From 8ebf6c840bca0d5459042ec66ca9a3745dcdd7dc Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 19 Jan 2025 16:01:34 +0100 Subject: [PATCH 606/682] chore:Upgrade packages --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ba0c812..1080b336 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,12 +20,12 @@ - - + + - + From 79d18dea48a6389fe1b87ea4bdbaee70cc6699a7 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 27 Jan 2025 10:50:13 +0100 Subject: [PATCH 607/682] chore: Upgrade packages --- Directory.Packages.props | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1080b336..e31e23ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -34,12 +34,18 @@ - - + + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -47,4 +53,4 @@ - + \ No newline at end of file From 693611978e1c8b3a2d650bd904811cc625e90ec5 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 27 Jan 2025 10:52:30 +0100 Subject: [PATCH 608/682] feat: Run ci against multiple env's --- .github/workflows/dotnet.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index cdca14d4..4a12996d 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,7 +13,11 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest ] steps: - uses: actions/checkout@v4 From 54e688c7406b9c81f7e6490e9183b0419c4ef545 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 28 Jan 2025 07:56:21 +0100 Subject: [PATCH 609/682] exclude windows until fix --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4a12996d..eb946f9d 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest ] + os: [ ubuntu-latest, ubuntu-24.04-arm, macos-latest ] steps: - uses: actions/checkout@v4 From fca124d5582aad94331f53bf40eeb9a1f1adf095 Mon Sep 17 00:00:00 2001 From: Elias Mascheroni <6045426+EliasMasche@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:49:53 -0300 Subject: [PATCH 610/682] feat: Added Initial PostgreSQL support (#399) * Added Initial PostgreSQL support * Updated with PostgreSql support * Update Configuration.md with PostgreSql support * Update BlogPost.cs with UtcNow * Added ScheduledPublishedDate to UTC --------- Co-authored-by: Steven Giesel --- Directory.Packages.props | 1 + docs/Setup/Configuration.md | 4 ++-- docs/Storage/Readme.md | 12 ++++++++++-- src/LinkDotNet.Blog.Domain/BlogPost.cs | 2 +- .../LinkDotNet.Blog.Infrastructure.csproj | 1 + .../Persistence/PersistenceProvider.cs | 1 + .../Components/CreateNewBlogPost.razor | 1 + .../Components/CreateNewModel.cs | 6 +++--- .../SqlRegistrationExtensions.cs | 18 ++++++++++++++++++ .../StorageProviderExtensions.cs | 5 +++++ ...orageProviderRegistrationExtensionsTests.cs | 3 ++- 11 files changed, 45 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e31e23ac..7f4f9bdc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + diff --git a/docs/Setup/Configuration.md b/docs/Setup/Configuration.md index a202beed..28704dd5 100644 --- a/docs/Setup/Configuration.md +++ b/docs/Setup/Configuration.md @@ -82,7 +82,7 @@ The appsettings.json file has a lot of options to customize the content of the b | Description | MarkdownString | Small introduction text for yourself. This is also used for `` tag. For this the markup will be converted to plain text | | BackgroundUrl | string | Url or path to the background image. (Optional) | | ProfilePictureUrl | string | Url or path to your profile picture | -| [PersistenceProvider](./../Storage/Readme.md) | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `MongoDB`, `MySql`). More in-depth explanation [here](./../Storage/Readme.md) | +| [PersistenceProvider](./../Storage/Readme.md) | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `MongoDB`, `MySql`, `PostgreSql`). More in-depth explanation [here](./../Storage/Readme.md) | | ConnectionString | string | Is used for connection to a database. | | DatabaseName | string | Name of the database. Only used with `RavenDbStorageProvider` | | [AuthProvider](./../Authorization/Readme.md) | string | | @@ -108,4 +108,4 @@ The appsettings.json file has a lot of options to customize the content of the b | ConnectionString | string | The connection string for the image storage provider. Only used if `AuthenticationMode` is set to `ConnectionString` | | ServiceUrl | string | The host url of the Azure blob storage. Only used if `AuthenticationMode` is set to `Default` | | 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. | \ No newline at end of file +| 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. | diff --git a/docs/Storage/Readme.md b/docs/Storage/Readme.md index 74772ef2..b5cf48be 100644 --- a/docs/Storage/Readme.md +++ b/docs/Storage/Readme.md @@ -7,6 +7,7 @@ Currently, there are 5 Storage-Provider: - Sqlite - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created. - SqlServer - Based on EF Core, it can be easily adapted for other Sql Dialects. The tables are automatically created. - MySql - Based on EF Core - also supports MariaDB. +- PostgreSql - Based on EF Core. The default (when you clone the repository) is the `Sqlite` option with an in-memory database. That means every time you restart the service, all posts and related objects are gone. This is useful for testing. @@ -31,9 +32,16 @@ For MySql use the following: "ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" ``` +For PostgreSql use the following: + +``` +"PersistenceProvider": "PostgreSql" +"ConnectionString": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE" +``` + ## Entity Framework Migrations -For the SQL providers (`SqlServer`, `Sqlite`, `MySql`), you can use Entity Framework Core Migrations to create and manage the database schema. The whole documentation can be found under [*"Entity Framework Core tools reference"*](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). The short version is that you can use the following steps: +For the SQL providers (`SqlServer`, `Sqlite`, `MySql`, `PostgreSql`), you can use Entity Framework Core Migrations to create and manage the database schema. The whole documentation can be found under [*"Entity Framework Core tools reference"*](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). The short version is that you can use the following steps: ```bash dotnet ef database update --project src/LinkDotNet.Blog.Infrastructure --startup-project src/LinkDotNet.Blog.Web --connection "" @@ -51,4 +59,4 @@ Here is the full documentation: [*"Applying Migrations"*](https://learn.microsof Alternatively, the blog calls `Database.EnsureCreated()` on startup, which creates the database schema if it does not exist. So you are not forced to use migrations. ## Considerations -For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). \ No newline at end of file +For most people a Sqlite database might be the best choice between convienence and ease of setup. As it runs "in-process" there are no additional dependencies or setup required (and therefore no additional cost). As the blog tries to cache many things, the load onto the database is not that big (performance considerations). The advantages of a "real" database like SqlServer or MySql are more in the realm of backups, replication, and other enterprise features (which are not needed often times for a simple blog). diff --git a/src/LinkDotNet.Blog.Domain/BlogPost.cs b/src/LinkDotNet.Blog.Domain/BlogPost.cs index 725efb27..3a52b28e 100644 --- a/src/LinkDotNet.Blog.Domain/BlogPost.cs +++ b/src/LinkDotNet.Blog.Domain/BlogPost.cs @@ -99,7 +99,7 @@ public static BlogPost Create( throw new InvalidOperationException("Can't schedule publish date if the blog post is already published."); } - var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.Now; + var blogPostUpdateDate = scheduledPublishDate ?? updatedDate ?? DateTime.UtcNow; var blogPost = new BlogPost { diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj index b23c2383..ea9e16e2 100644 --- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj +++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj @@ -14,6 +14,7 @@ + diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs index 8ea776d5..01f247d7 100644 --- a/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs +++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/PersistenceProvider.cs @@ -9,6 +9,7 @@ public sealed class PersistenceProvider : Enumeration public static readonly PersistenceProvider RavenDb = new(nameof(RavenDb)); public static readonly PersistenceProvider MySql = new(nameof(MySql)); public static readonly PersistenceProvider MongoDB = new(nameof(MongoDB)); + public static readonly PersistenceProvider PostgreSql = new(nameof(PostgreSql)); private PersistenceProvider(string key) : base(key) diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index e028410f..ad217d01 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -73,6 +73,7 @@ If set the blog post will be published at the given date. A blog post with a schedule date can't be set to published. + All dates are stored in UTC internally.
    diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index 6c7c5153..0a71e1ff 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -67,8 +67,8 @@ public bool ShouldUpdateDate [FutureDateValidation] public DateTime? ScheduledPublishDate { - get => scheduledPublishDate; - set => SetProperty(out scheduledPublishDate, value); + get => scheduledPublishDate?.ToLocalTime(); + set => SetProperty(out scheduledPublishDate, value?.ToUniversalTime()); } public string Tags @@ -108,7 +108,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost) PreviewImageUrl = blogPost.PreviewImageUrl, originalUpdatedDate = blogPost.UpdatedDate, PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback ?? string.Empty, - ScheduledPublishDate = blogPost.ScheduledPublishDate, + scheduledPublishDate = blogPost.ScheduledPublishDate?.ToUniversalTime(), IsDirty = false, }; } diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs index 8787666d..463327da 100644 --- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs +++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/SqlRegistrationExtensions.cs @@ -66,4 +66,22 @@ public static void UseMySqlAsStorageProvider(this IServiceCollection services) }); services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); } + + public static void UsePostgreSqlAsStorageProvider(this IServiceCollection services) + { + services.AssertNotAlreadyRegistered(typeof(IRepository<>)); + + services.AddPooledDbContextFactory( + (s, builder) => + { + var configuration = s.GetRequiredService>(); + var connectionString = configuration.Value.ConnectionString; + builder.UseNpgsql(connectionString) +#if DEBUG + .EnableDetailedErrors() +#endif + ; + }); + services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + } } diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs index 348eeafc..9498d37d 100644 --- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs +++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs @@ -43,6 +43,11 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv services.UseMongoDBAsStorageProvider(); services.RegisterCachedRepository>(); } + else if (persistenceProvider == PersistenceProvider.PostgreSql) + { + services.UsePostgreSqlAsStorageProvider(); + services.RegisterCachedRepository>(); + } return services; } diff --git a/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs b/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs index aad788cc..3bf125fc 100644 --- a/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/StorageProviderRegistrationExtensionsTests.cs @@ -11,7 +11,8 @@ public class StorageProviderRegistrationExtensionsTests services => services.UseSqliteAsStorageProvider(), services => services.UseSqlAsStorageProvider(), services => services.UseRavenDbAsStorageProvider(), - services => services.UseMySqlAsStorageProvider() + services => services.UseMySqlAsStorageProvider(), + services => services.UsePostgreSqlAsStorageProvider() }; [Theory] From 67a10370f8572af7959113da79c5d6195346e410 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 31 Jan 2025 12:41:46 +0100 Subject: [PATCH 611/682] chore: Update Npgsql and NCronJob package versions --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7f4f9bdc..94ae5e3c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + @@ -26,7 +26,7 @@ - + From 90663f4e8cd31c5e046a70b7b7fa6ec2a959e267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 08:17:15 +0000 Subject: [PATCH 612/682] chore(deps): bump SonarAnalyzer.CSharp Bumps [SonarAnalyzer.CSharp](https://github.com/SonarSource/sonar-dotnet) from 10.5.0.109200 to 10.6.0.109712. - [Release notes](https://github.com/SonarSource/sonar-dotnet/releases) - [Commits](https://github.com/SonarSource/sonar-dotnet/compare/10.5.0.109200...10.6.0.109712) --- updated-dependencies: - dependency-name: SonarAnalyzer.CSharp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 94ae5e3c..b648f30c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + From 96909137e7a4fb1048a130a4b2d754d8bb0f4d7d Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 19 Feb 2025 13:06:12 +0100 Subject: [PATCH 613/682] chore: Upgrade packages --- Directory.Packages.props | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b648f30c..8b11229a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,34 +8,34 @@ - - - - - + + + + + - + - - + + - - + + - + - - + + - + @@ -52,6 +52,6 @@ - + \ No newline at end of file From a098f7051c173ec95ce211205d5181dc8bd32dac Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Thu, 20 Feb 2025 20:08:38 +0100 Subject: [PATCH 614/682] fix: Remove slnx as it makes problems with the build --- LinkDotNet.Blog.slnx | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 LinkDotNet.Blog.slnx diff --git a/LinkDotNet.Blog.slnx b/LinkDotNet.Blog.slnx deleted file mode 100644 index 925fd302..00000000 --- a/LinkDotNet.Blog.slnx +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 8566b28d74f32082c7a3f59c6455e9e3c4aabd42 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 21 Feb 2025 09:45:28 +0100 Subject: [PATCH 615/682] fix: Remove (temporarly) interceptors --- .../Components/VisitCountPerPageTests.cs | 9 +++++- .../SearchByTag/SearchByTagPageTests.cs | 9 +++++- .../Web/Shared/NavMenuTests.cs | 10 +++++-- .../ShowBlogPost/ShowBlogPostPageTests.cs | 28 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs index f51ec08c..7efcd498 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/Dashboard/Components/VisitCountPerPageTests.cs @@ -7,6 +7,7 @@ using LinkDotNet.Blog.Infrastructure.Persistence.Sql; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.Dashboard.Components; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using TestContext = Xunit.TestContext; @@ -54,7 +55,7 @@ public async Task ShouldFilterByDate() await DbContext.BlogPostRecords.AddRangeAsync(clicked1, clicked2, clicked3, clicked4); await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); await using var ctx = new BunitContext(); - ctx.ComponentFactories.AddStub(); + ctx.ComponentFactories.Add(); RegisterRepositories(ctx); var cut = ctx.Render(); var filter = new Filter { StartDate = new DateOnly(2019, 1, 1), EndDate = new DateOnly(2020, 12, 31) }; @@ -136,4 +137,10 @@ private async Task SaveBlogPostArticleClicked(string blogPostId, int count) await DbContext.SaveChangesAsync(); } + + private class DateRangeSelectorStub : ComponentBase + { + [Parameter] + public EventCallback FilterChanged { get; set; } + } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs index 50b3fe00..325d039f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs @@ -3,6 +3,7 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.SearchByTag; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; @@ -46,7 +47,7 @@ public void ShouldSetTitleToTag() { using var ctx = new BunitContext(); ctx.Services.AddScoped(_ => Repository); - ctx.ComponentFactories.AddStub(); + ctx.ComponentFactories.Add(); var cut = ctx.Render(p => p.Add(s => s.Tag, "Tag")); @@ -66,4 +67,10 @@ private void RegisterServices(BunitContext ctx) { ctx.Services.AddScoped(_ => Repository); } + + private class PageTitleStub : ComponentBase + { + [Parameter] + public RenderFragment? ChildContent { get; set; } = default!; + } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs index 27e47dab..b91904cc 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Shared/NavMenuTests.cs @@ -12,7 +12,7 @@ public class NavMenuTests : BunitContext { public NavMenuTests() { - ComponentFactories.AddStub(); + ComponentFactories.Add(); } [Fact] @@ -101,6 +101,12 @@ public void ShouldShowBlogNameWhenNotBrand(string? brandUrl) var brandImage = cut.Find(".nav-brand"); var image = brandImage as IHtmlAnchorElement; image.ShouldNotBeNull(); - image!.TextContent.ShouldBe("Steven"); + image.TextContent.ShouldBe("Steven"); + } + + private class ThemeTogglerStub : ComponentBase + { + [Parameter] + public string Class { get; set; } = default!; } } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index bc013a40..d661af9b 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -9,6 +9,7 @@ using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -21,7 +22,7 @@ public class ShowBlogPostPageTests : BunitContext { public ShowBlogPostPageTests() { - ComponentFactories.AddStub(); + ComponentFactories.Add(); JSInterop.Mode = JSRuntimeMode.Loose; var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); @@ -31,8 +32,8 @@ public ShowBlogPostPageTests() Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); AddAuthorization(); - ComponentFactories.AddStub(); - ComponentFactories.AddStub(); + ComponentFactories.Add(); + ComponentFactories.Add(); ComponentFactories.AddStub(); } @@ -161,4 +162,25 @@ public void ShouldSetCanoncialUrlOfOgDataWithoutSlug() cut.FindComponent().Instance.CanonicalRelativeUrl.ShouldBe("blogPost/1"); } + + private class PageTitleStub : ComponentBase + { + [Parameter] + public RenderFragment? ChildContent { get; set; } = default!; + } + + private class LikeStub : ComponentBase + { + [Parameter] + public BlogPost BlogPost { get; set; } = default!; + + [Parameter] + public EventCallback OnBlogPostLiked { get; set; } = default!; + } + + private class SimilarBlogPostSectionStub : ComponentBase + { + [Parameter] + public BlogPost BlogPost { get; set; } = default!; + } } \ No newline at end of file From 988b3065f2ce1be844b28025cbd11b436ad53e45 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 1 Mar 2025 09:52:53 +0100 Subject: [PATCH 616/682] chore: Update packages (fixes #402) --- Directory.Packages.props | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8b11229a..4b04d40e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -15,18 +15,18 @@ - + - - + + - + @@ -54,4 +54,4 @@ - \ No newline at end of file + From f8b1cdda7df0f82ad77aca604dbb775b854aaf87 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 1 Mar 2025 09:56:25 +0100 Subject: [PATCH 617/682] chore: Update test packages --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b04d40e..eaa17b71 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,8 +47,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 74a7dce0fda893665965be1f56c08060087fe3a3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 3 Mar 2025 20:19:47 +0100 Subject: [PATCH 618/682] chore: Upgrade packages --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eaa17b71..64828643 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,7 @@ - + @@ -47,7 +47,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From bbd18915fe53eb63a4ebd32b7861f9903c4e1252 Mon Sep 17 00:00:00 2001 From: Levi Noeninckx <64925459+levinoeninckx@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:12:33 +0000 Subject: [PATCH 619/682] feat: Bookmarks (#403) feat: Added bookmarks feature (closes #367) --- src/LinkDotNet.Blog.Web/App.razor | 21 ++--- .../Features/Bookmarks/BookmarkService.cs | 63 ++++++++++++++ .../Features/Bookmarks/Bookmarks.razor | 46 ++++++++++ .../Bookmarks/Components/BookmarkButton.razor | 17 ++++ .../Features/Bookmarks/IBookmarkService.cs | 11 +++ .../Features/Components/ShortBlogPost.razor | 29 ++++++- .../Components/ShortBlogPost.razor.css | 5 ++ .../Features/Home/Components/NavMenu.razor | 12 +-- .../ShowBlogPost/ShowBlogPostPage.razor | 30 ++++++- src/LinkDotNet.Blog.Web/Pages/_Host.cshtml | 2 +- src/LinkDotNet.Blog.Web/ServiceExtensions.cs | 2 + .../wwwroot/css/fonts/Blog.json | 10 ++- .../wwwroot/css/fonts/icons.woff | Bin 5508 -> 5556 bytes .../wwwroot/css/fonts/icons.woff2 | Bin 2828 -> 2876 bytes src/LinkDotNet.Blog.Web/wwwroot/css/icons.css | 3 + .../DraftBlogPost/DraftBlogPostPageTests.cs | 2 + .../Web/Features/Bookmarks/BookmarksTests.cs | 82 ++++++++++++++++++ .../Web/Features/Home/IndexTests.cs | 2 + .../Web/Features/Search/SearchPageTests.cs | 2 + .../SearchByTag/SearchByTagPageTests.cs | 2 + .../ShowBlogPost/ShowBlogPostPageTests.cs | 2 + .../Bookmarks/BookmarkServiceTests.cs | 81 +++++++++++++++++ .../Features/Components/ShortBlogPostTests.cs | 8 ++ .../ShowBlogPost/ShowBlogPostPageTests.cs | 2 + 24 files changed, 409 insertions(+), 25 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs create mode 100644 src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor create mode 100644 src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor create mode 100644 src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs create mode 100644 tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs diff --git a/src/LinkDotNet.Blog.Web/App.razor b/src/LinkDotNet.Blog.Web/App.razor index 6b1f7d27..074d2b2f 100644 --- a/src/LinkDotNet.Blog.Web/App.razor +++ b/src/LinkDotNet.Blog.Web/App.razor @@ -1,13 +1,14 @@ @using LinkDotNet.Blog.Web.Features.Home.Components + - - - - - - - - - - + + + + + + + + + + diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs new file mode 100644 index 00000000..189fc28b --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs @@ -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 IsBookmarked(string postId) + { + ArgumentException.ThrowIfNullOrEmpty(postId); + await InitializeIfNotExists(); + var bookmarks = await localStorageService.GetItemAsync>("bookmarks"); + + return bookmarks.Contains(postId); + } + + public async Task> GetBookmarkedPostIds() + { + await InitializeIfNotExists(); + return await localStorageService.GetItemAsync>("bookmarks"); + } + + public async Task SetBookmark(string postId, bool isBookmarked) + { + ArgumentException.ThrowIfNullOrEmpty(postId); + await InitializeIfNotExists(); + + var bookmarks = await localStorageService.GetItemAsync>("bookmarks"); + + if (!isBookmarked) + { + bookmarks.Remove(postId); + } + else + { + bookmarks.Add(postId); + } + + await localStorageService.SetItemAsync("bookmarks", bookmarks); + + } + + private async Task InitializeIfNotExists() + { + if (!(await localStorageService.ContainKeyAsync("bookmarks"))) + { + await localStorageService.SetItemAsync("bookmarks", new List()); + } + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor new file mode 100644 index 00000000..8ffcef82 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Bookmarks.razor @@ -0,0 +1,46 @@ +@page "/bookmarks" +@using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Infrastructure.Persistence +@inject IBookmarkService BookmarkService +@inject IRepository BlogPostRepository; + +
    +

    Bookmarks

    + @if (bookmarkedPosts.Count <= 0) + { + + } + else + { + @foreach (var post in bookmarkedPosts) + { + + } + } +
    + +@code { + private IReadOnlyList 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(); + } + } + } + +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor new file mode 100644 index 00000000..0b1800b9 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/Components/BookmarkButton.razor @@ -0,0 +1,17 @@ + + +@code { + [Parameter] public bool IsBookmarked { get; set; } + [Parameter] public EventCallback Bookmarked { get; set; } + + private async Task OnBookmarkClicked() + { + await Bookmarked.InvokeAsync(); + } +} diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs b/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs new file mode 100644 index 00000000..35e98179 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/IBookmarkService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LinkDotNet.Blog.Web.Features.Bookmarks; + +public interface IBookmarkService +{ + public Task IsBookmarked(string postId); + public Task> GetBookmarkedPostIds(); + public Task SetBookmark(string postId, bool isBookmarked); +} diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index ed5e67a6..e1345977 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -1,4 +1,7 @@ @using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Web.Features.Bookmarks +@using LinkDotNet.Blog.Web.Features.Bookmarks.Components +@inject IBookmarkService BookmarkService
    @@ -47,6 +52,8 @@ [Parameter, EditorRequired] public required BlogPost BlogPost { get; set; } + private bool isBookmarked = false; + [Parameter] public bool UseAlternativeStyle { get; set; } @@ -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) @@ -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(); + } + } } diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css index 173a7a5e..b0bb87ea 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css @@ -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; diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor index 27cf67bb..c2ca1329 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/NavMenu.razor @@ -23,9 +23,11 @@ - @BlogPost.ReadingTimeInMinutes minute read + @BlogPost.ReadingTimeInMinutes minute read +
    + +
    @if (BlogPost.Tags is not null && BlogPost.Tags.Any()) {
    @@ -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 shortCodes = []; + private bool isBookmarked; private BlogPost? BlogPost { get; set; } @@ -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) @@ -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(); + } + } diff --git a/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml b/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml index a9d61e28..9b8117f2 100644 --- a/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/_Host.cshtml @@ -5,4 +5,4 @@ Layout = "_Layout"; } - \ No newline at end of file + diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index 6432c7d0..ef4a1c01 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -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; @@ -19,6 +20,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json index 6fa6177b..ba5db898 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/Blog.json @@ -1929,7 +1929,10 @@ "ligatures": "bookmark, ribbon", "name": "bookmark", "id": 210, - "order": 0 + "order": 87, + "prevSize": 32, + "code": 59858, + "tempChar": "" }, { "ligatures": "bookmarks, ribbons", @@ -3150,7 +3153,7 @@ "order": 85, "prevSize": 32, "code": 60061, - "tempChar": "" + "tempChar": "" }, { "ligatures": "youtube2, brand22", @@ -13357,6 +13360,5 @@ "showCodes": true, "gridSize": 16 }, - "uid": -1, - "time": 1731146188893 + "uid": -1 } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff index 40f27b1e8111bedd22d6bf3015b0ca8dc75d8c5c..e6709dc1dbaefe284e3893b095dca86293896452 100644 GIT binary patch delta 423 zcmZqC-l8p5?(gQtz{mguqFWfa!E^=#<77i7v57iz^^7bG42&#F0uR~K6N?KN7?^T^ zDmb86AU&rt4JcN^z#vco!n>|4wa-XROkrRUdIMBr2EsZF=QA^af(#78DnLFMa~xuM zkda#g)FbQx6srN@;syqWocv^vx+6fo0|=WLb6FkAO{@S~EW!d5Pyk~chP1rI+*F_# z&@je45Wd)yJF_6a80ZkuDHH#6>%IUgGy^JUVGslQP2nZeOV*bPFO^^Fyfk}h{nGzs z@XNB7Z7(OhT=jD8%S)3tF`6@8n*4&%RMc6_QH)pgn&@=V7SVdqT+vVlW^h=TY))dj z&8fu5!0`V7vjg)1pur5nf}D(k2N)d~f&4!Vj1K<}00lQ&^YpUGuzA)u$Mf5KW#9(6 znt|c*hofaMdh!z?b4K~eGQt&;6NFhNPZ8FUV)@1Dz*@$7i%o-V0oyP3IQDlOHb7@H MFvxGdCA^9e000?nWdHyG delta 366 zcmdm@-J&g4?(gQtz{mguqAd*EVA_I#ak3+m*hC$b8ua+5uTD<&Ti){$b>V@+W_!6wGm$M%BVhy4zR7EmE05X)`m I5?RFv09RpFaR2}S diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 b/src/LinkDotNet.Blog.Web/wwwroot/css/fonts/icons.woff2 index a4f1ee4cc307a45d8d602a733432e615da3b5d44..4953dd2889cf1587b66ceb83c02e3ec6ef594c5f 100644 GIT binary patch delta 2871 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 VQ{PTVt}}bw_u?8$I1F0c}+yGh=+e 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 Y&FPn%=$_`EZg+RPdHh^9n>!r<077O>TL1t6 diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css b/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css index 1a65e5e8..fca2aad1 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/icons.css @@ -112,3 +112,6 @@ i { .bluesky:before { content: "\e902"; } +.bookmark:before { + content: "\e9d2"; +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs index cca5020c..b932a383 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs @@ -2,6 +2,7 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Features.Admin.DraftBlogPost; + using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,7 @@ public async Task ShouldOnlyShowPublishedPosts() using var ctx = new BunitContext(); ctx.JSInterop.Mode = JSRuntimeMode.Loose; ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); var cut = ctx.Render(); cut.WaitForElement(".blog-card"); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs new file mode 100644 index 00000000..88d67b82 --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkDotNet.Blog.Domain; +using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; +using LinkDotNet.Blog.Web.Features.Bookmarks.Components; +using LinkDotNet.Blog.Web.Features.Components; +using Microsoft.Extensions.DependencyInjection; + +namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Bookmarks; + +public class BookmarksTests : SqlDatabaseTestBase +{ + [Fact] + public async Task ShouldOnlyDisplayBookmarkedPosts() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + var bookmarkedBlogPost = new BlogPostBuilder().WithTitle("Bookmarked Post").Build(); + var nonBookmarkedBlogPost = new BlogPostBuilder().WithTitle("Non-Bookmarked Post").Build(); + await Repository.StoreAsync(bookmarkedBlogPost); + await Repository.StoreAsync(nonBookmarkedBlogPost); + bookmarkService.GetBookmarkedPostIds().Returns(new List { bookmarkedBlogPost.Id }); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + + // Assert + cut.WaitForElement(".blog-card"); + var blogPosts = cut.FindComponents(); + + blogPosts.ShouldHaveSingleItem(); + blogPosts[0].Find(".description h1").TextContent.ShouldBe("Bookmarked Post"); + } + + [Fact] + public async Task ShouldRemoveBookmarkWhenButtonIsClicked() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + var bookmarkedBlogPost = new BlogPostBuilder().WithTitle("Bookmarked Post").Build(); + await Repository.StoreAsync(bookmarkedBlogPost); + bookmarkService.GetBookmarkedPostIds().Returns(new List { bookmarkedBlogPost.Id }); + bookmarkService.IsBookmarked(bookmarkedBlogPost.Id).Returns(true); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + cut.WaitForElement(".blog-card"); + + // Find and click the bookmark button + var bookmarkButton = cut.FindComponent().Find("button"); + bookmarkButton.Click(); + + // Assert + await bookmarkService.Received(1).SetBookmark(bookmarkedBlogPost.Id, false); + } + + [Fact] + public void ShouldDisplayMessageWhenNoBookmarksExist() + { + // Arrange + using var ctx = new BunitContext(); + var bookmarkService = Substitute.For(); + bookmarkService.GetBookmarkedPostIds().Returns(new List()); + ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => bookmarkService); + + // Act + var cut = ctx.Render(); + + // Assert + cut.WaitForElement("p"); + cut.Find("p").TextContent.ShouldBe("You have no bookmarks"); + cut.FindComponents().Count.ShouldBe(0); + } +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index b52f3ff3..a5a64d29 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs @@ -3,6 +3,7 @@ using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Home; using LinkDotNet.Blog.Web.Features.Services; @@ -168,5 +169,6 @@ private void RegisterComponents(BunitContext ctx, string? profilePictureUri = nu ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).ApplicationConfiguration)); ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).Introduction)); ctx.Services.AddScoped(_ => Substitute.For()); + ctx.Services.AddScoped(_ => Substitute.For()); } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs index bab5d7e4..8813ba5f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Search; using Microsoft.Extensions.DependencyInjection; @@ -58,5 +59,6 @@ public async Task ShouldUnescapeQuery() private void RegisterServices(BunitContext ctx) { ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs index 325d039f..5628328e 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/SearchByTag/SearchByTagPageTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using LinkDotNet.Blog.Domain; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.SearchByTag; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -66,6 +67,7 @@ private async Task AddBlogPostWithTagAsync(string tag, bool isPublished = true) private void RegisterServices(BunitContext ctx) { ctx.Services.AddScoped(_ => Repository); + ctx.Services.AddScoped(_ => Substitute.For()); } private class PageTitleStub : ComponentBase diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index 22923471..817c6d6f 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -4,6 +4,7 @@ using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; @@ -148,5 +149,6 @@ private void RegisterComponents(BunitContext ctx, ILocalStorageService? localSto var shortCodeRepository = Substitute.For>(); shortCodeRepository.GetAllAsync().Returns(PagedList.Empty); ctx.Services.AddScoped(_ => shortCodeRepository); + ctx.Services.AddScoped(_ => Substitute.For()); } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs new file mode 100644 index 00000000..903ad59e --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Bookmarks/BookmarkServiceTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LinkDotNet.Blog.Web.Features.Bookmarks; +using LinkDotNet.Blog.Web.Features.Services; +using NSubstitute.ReceivedExtensions; + +namespace LinkDotNet.Blog.UnitTests.Web.Features.Bookmarks; + +public class BookmarkServiceTests +{ + private readonly ILocalStorageService localStorageService; + private readonly IBookmarkService bookmarkService; + + public BookmarkServiceTests() + { + localStorageService = Substitute.For(); + bookmarkService = new BookmarkService(localStorageService); + } + + [Fact] + public async Task ShouldReturnIds() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var ids = await bookmarkService.GetBookmarkedPostIds(); + + ids.ShouldBe(["1", "2", "3"]); + ids.ShouldBeUnique(); + } + + [Fact] + public async Task ShouldReturnEmptyListWhenNoBookmarks() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => []); + + var ids = await bookmarkService.GetBookmarkedPostIds(); + + ids.ShouldBeEmpty(); + } + + [Fact] + public async Task ShouldReturnTrueIfBookmarked() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var isBookmarked = await bookmarkService.IsBookmarked("1"); + + isBookmarked.ShouldBeTrue(); + } + + [Fact] + public async Task ShouldReturnFalseIfNoBookmarked() + { + localStorageService + .GetItemAsync>("bookmarks") + .Returns(x => ["1", "2", "3"]); + + var isBookmarked = await bookmarkService.IsBookmarked("4"); + + isBookmarked.ShouldBeFalse(); + } + + [Fact] + public async Task ShouldThrowArgumentExceptionWhenIdIsEmptyOrNull() + { + var id = string.Empty; + + await bookmarkService.SetBookmark(id, false) + .ShouldThrowAsync(); + + await bookmarkService.IsBookmarked(id) + .ShouldThrowAsync(); + } +} diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs index d70d755b..b3d4511d 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/Components/ShortBlogPostTests.cs @@ -1,7 +1,9 @@ using System; using System.Linq; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; +using Microsoft.Extensions.DependencyInjection; namespace LinkDotNet.Blog.UnitTests.Web.Features.Components; @@ -10,6 +12,7 @@ public class ShortBlogPostTests : BunitContext [Fact] public void ShouldOpenBlogPost() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().Build(); blogPost.Id = "SomeId"; var cut = Render( @@ -23,6 +26,7 @@ public void ShouldOpenBlogPost() [Fact] public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().WithTags("Tag 1").Build(); var cut = Render( p => p.Add(c => c.BlogPost, blogPost)); @@ -35,6 +39,7 @@ public void ShouldNavigateToEscapedTagSiteWhenClickingOnTag() [Fact] public void WhenNoTagsAreGivenThenTagsAreNotShown() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().Build(); var cut = Render( @@ -46,6 +51,7 @@ public void WhenNoTagsAreGivenThenTagsAreNotShown() [Fact] public void GivenBlogPostThatIsScheduled_ThenIndicating() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(false).WithScheduledPublishDate(new DateTime(2099, 1, 1)) .Build(); @@ -58,6 +64,7 @@ public void GivenBlogPostThatIsScheduled_ThenIndicating() [Fact] public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(false).Build(); var cut = Render( @@ -69,6 +76,7 @@ public void GivenBlogPostThatIsNotPublishedAndNotScheduled_ThenIndicating() [Fact] public void GivenBlogPostThatIsPublished_ThenNoDraft() { + Services.AddScoped(_ => Substitute.For()); var blogPost = new BlogPostBuilder().IsPublished(true).Build(); var cut = Render( diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs index d661af9b..98baee9d 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Features/ShowBlogPost/ShowBlogPostPageTests.cs @@ -5,6 +5,7 @@ using LinkDotNet.Blog.Infrastructure; using LinkDotNet.Blog.Infrastructure.Persistence; using LinkDotNet.Blog.TestUtilities; +using LinkDotNet.Blog.Web.Features.Bookmarks; using LinkDotNet.Blog.Web.Features.Components; using LinkDotNet.Blog.Web.Features.Services; using LinkDotNet.Blog.Web.Features.ShowBlogPost; @@ -31,6 +32,7 @@ public ShowBlogPostPageTests() Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => Substitute.For()); Services.AddScoped(_ => Options.Create(new ApplicationConfigurationBuilder().Build())); + Services.AddScoped(_ => Substitute.For()); AddAuthorization(); ComponentFactories.Add(); ComponentFactories.Add(); From b67fa662fb799edaa89f1f93c32736d2f4701a03 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 7 Mar 2025 17:20:23 +0100 Subject: [PATCH 620/682] fix: Test --- .../Web/Features/Bookmarks/BookmarksTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs index 88d67b82..aac491c1 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs @@ -75,8 +75,8 @@ public void ShouldDisplayMessageWhenNoBookmarksExist() var cut = ctx.Render(); // Assert - cut.WaitForElement("p"); - cut.Find("p").TextContent.ShouldBe("You have no bookmarks"); + cut.WaitForElement("h4"); + cut.Find("h4").TextContent.ShouldBe("No bookmarks yet!"); cut.FindComponents().Count.ShouldBe(0); } } From 29e809f80c2d99c60d1fe7c7a3493f865717dff3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 8 Mar 2025 14:01:37 +0100 Subject: [PATCH 621/682] ui/ux: Better Similiar Blog Posts --- .../Components/SimilarBlogPostSection.razor | 111 ++++++++++++------ .../ShowBlogPost/ShowBlogPostPage.razor.css | 6 - src/LinkDotNet.Blog.Web/wwwroot/css/basic.css | 6 + 3 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/SimilarBlogPostSection.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/SimilarBlogPostSection.razor index 2fbed6d2..93551300 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/SimilarBlogPostSection.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/SimilarBlogPostSection.razor @@ -1,50 +1,89 @@ @using LinkDotNet.Blog.Domain @using LinkDotNet.Blog.Infrastructure.Persistence +@using LinkDotNet.Blog.Web.Features @inject IRepository BlogPostRepository @inject IRepository SimilarBlogPostJobRepository @if (similarBlogPosts.Count > 0) { -
    -
    -

    - -

    -
    -
    - @foreach (var relatedBlogPost in similarBlogPosts) - { -
    -
    -
    -
    @relatedBlogPost.Title
    -

    @MarkdownConverter.ToMarkupString(relatedBlogPost.ShortDescription)

    -
    - -
    -
    - } -
    -
    -
    -
    + } @code { - [Parameter, EditorRequired] + [Parameter, EditorRequired] public required BlogPost BlogPost { get; set; } - private IReadOnlyCollection similarBlogPosts = []; + private IReadOnlyCollection similarBlogPosts = []; - protected override async Task OnParametersSetAsync() - { - var similarBlogPostIds = await SimilarBlogPostJobRepository.GetByIdAsync(BlogPost.Id); - if (similarBlogPostIds is not null) - { - similarBlogPosts = await BlogPostRepository.GetAllAsync( - b => similarBlogPostIds.SimilarBlogPostIds.Contains(b.Id)); - } - } + protected override async Task OnParametersSetAsync() + { + var similarBlogPostIds = await SimilarBlogPostJobRepository.GetByIdAsync(BlogPost.Id); + if (similarBlogPostIds is not null) + { + similarBlogPosts = await BlogPostRepository.GetAllAsync( + b => similarBlogPostIds.SimilarBlogPostIds.Contains(b.Id)); + } + } } diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css index 8d4442b4..a52a0558 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor.css @@ -23,12 +23,6 @@ object-fit: cover; } -.read-time:before { - font-family: 'icons'; - font-weight: 900; - content: "\e94f"; -} - @media only screen and (max-width: 700px) { .blog-outer-box .blog-container { width: 90%; diff --git a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css index 46b1bf02..50085337 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css +++ b/src/LinkDotNet.Blog.Web/wwwroot/css/basic.css @@ -111,6 +111,12 @@ code { content: "\e9d5"; } +.read-time:before { + font-family: 'icons'; + font-weight: 900; + content: "\e94f"; +} + .copy-btn { border-color: whitesmoke !important; } From 5cf131a4904da2bc793dd290a042f1e6a3b7b946 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 8 Mar 2025 14:05:11 +0100 Subject: [PATCH 622/682] fix: Tests --- .../ShowBlogPost/Components/SimilarBlogPostSectionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs index 4bb230e1..e273f7ff 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/ShowBlogPost/Components/SimilarBlogPostSectionTests.cs @@ -35,7 +35,7 @@ public async Task ShouldShowSimilarBlogPosts() var cut = context.Render(p => p.Add(s => s.BlogPost, blogPost1)); - var elements = cut.WaitForElements(".card-title"); + var elements = cut.WaitForElements("h6"); elements.Count.ShouldBe(2); elements.ShouldContain(p => p.TextContent == "Title 2"); elements.ShouldContain(p => p.TextContent == "Title 3"); From 531d16e294d1e9711df4de60627c96cf376afa62 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 8 Mar 2025 15:09:47 +0100 Subject: [PATCH 623/682] Add features --- Readme.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Readme.md b/Readme.md index 7b0680c4..6d97ca09 100644 --- a/Readme.md +++ b/Readme.md @@ -11,6 +11,23 @@ The basic idea is that the content creator writes their posts in markdown langua The markdown will then be translated into HTML and displayed to the client. This gives an easy entry to writing posts with all the flexibility markdown has. This also includes source code snippets. Highlighting is done via [highlight.js](https://highlightjs.org/) with the GitHub theme. +## Features + +- **Modern Markdown Editor** - Write blog posts with a feature-rich markdown editor +- **Bookmarks** - Allow readers to save their favorite articles +- **Drafts** - Save work in progress and continue later +- **Scheduled Publishing** - Plan ahead and publish automatically +- **Similar Blog Posts** - Recommend related content to readers +- **Comments** - Enable discussions +- **Media Upload** - Easily include images in your posts (Azure Blob Storage and CDN Support) +- **SEO Optimization** - Improve search engine visibility +- **Tag and Category System** - Organize content effectively +- **Search Functionality** - Help readers find specific content +- **Responsive Design** - Optimal viewing on all devices +- **About Me Page** - Customizable profile page that showcases skills and experience +- **RSS Feed** - Allow readers to subscribe to content updates +- **Visit Counter** - Get visitor counters for each blog post in the internal dashboard + ## In Action ![overview](assets/overview.gif) From 3873a2a85acbce96d6d1abdd084c0c1cf8e54a67 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 14 Mar 2025 15:04:53 +0100 Subject: [PATCH 624/682] chore: Update package versions --- Directory.Packages.props | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 64828643..a5d938ea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,12 +7,12 @@ - - - - - - + + + + + + @@ -24,15 +24,15 @@ - - - + + + - + - - + + From 53040db61e869ae6ce32c86e434dcb57388f1c96 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Fri, 14 Mar 2025 15:09:45 +0100 Subject: [PATCH 625/682] chore: Update Pomelo.EntityFrameworkCore.MySql package version to preview.3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a5d938ea..796af21c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From 4edf2de55415d25feef0ad5f2347a9a4aec6c1af Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 15 Mar 2025 19:39:22 +0100 Subject: [PATCH 626/682] feat: Refactor Like button and remove custom animation --- .../ShowBlogPost/Components/Like.razor | 21 ++++++++++++-- .../ShowBlogPost/Components/Like.razor.css | 26 ------------------ .../wwwroot/assets/ClapAnimation.webp | Bin 15158 -> 0 bytes 3 files changed, 18 insertions(+), 29 deletions(-) delete mode 100644 src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor.css delete mode 100644 src/LinkDotNet.Blog.Web/wwwroot/assets/ClapAnimation.webp diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor index 798fe3fe..2b69de55 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor @@ -1,8 +1,21 @@ @using LinkDotNet.Blog.Domain @using LinkDotNet.Blog.Web.Features.Services @inject ILocalStorageService LocalStorage - @code { @@ -16,7 +29,9 @@ private bool HasLiked { get; set; } - private string BtnClass => HasLiked ? "clap-active" : string.Empty; + private string ButtonClass => HasLiked + ? "btn-primary" + : "btn-outline-secondary"; protected override void OnParametersSet() { diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor.css b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor.css deleted file mode 100644 index 571b34a4..00000000 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor.css +++ /dev/null @@ -1,26 +0,0 @@ -.clap-btn { - display: inline-block; - Cursor: pointer; - width: 32px; - height: 32px; - background: url('assets/ClapAnimation.webp') no-repeat 0 50%; - background-size: 900%; -} - -html[data-bs-theme='light'] .clap-btn { - filter: invert(100%); -} - -.clap-active { - animation-name: animate; - animation-duration: .8s; - animation-iteration-count: 1; - animation-fill-mode: forwards; - animation-timing-function: steps(8); -} - -@keyframes animate { - 0% { background-position: left; } - 50% { background-position: right; } - 100% { background-position: right; } - } diff --git a/src/LinkDotNet.Blog.Web/wwwroot/assets/ClapAnimation.webp b/src/LinkDotNet.Blog.Web/wwwroot/assets/ClapAnimation.webp deleted file mode 100644 index f7fafdfba2e2aaa4532f96e7772549beeed68ca3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15158 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 OmNeljfA{;L<^Kio`vDdJ From 12a108b1f5a8d585e90c5c81d32cea75e4e5a186 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sat, 15 Mar 2025 20:26:25 +0100 Subject: [PATCH 627/682] feat: Smaller ui adjustments for short card --- .../Features/Components/ShortBlogPost.razor | 4 ++-- .../Components/ShortBlogPost.razor.css | 18 +++++++++--------- .../DraftBlogPost/DraftBlogPostPageTests.cs | 2 +- .../Web/Features/Bookmarks/BookmarksTests.cs | 2 +- .../Web/Features/Home/IndexTests.cs | 6 +++--- .../Web/Features/Search/SearchPageTests.cs | 6 +++--- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor index e1345977..0be7b840 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor @@ -37,10 +37,10 @@
    -

    @BlogPost.Title

    +

    @BlogPost.Title

    -

    @MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)

    +

    @MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)

    Read the whole article

    diff --git a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css index b0bb87ea..23158dea 100644 --- a/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css +++ b/src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css @@ -85,11 +85,6 @@ justify-content: space-between; } -.blog-card .description h1 { - line-height: 1; - margin: 0 0 5px 0; - font-size: 1.7rem; -} .blog-card .description .read-more { text-align: right; } @@ -155,11 +150,16 @@ background-color: #ff8700; } -@media (max-width: 400px) { - .blog-card .description h1 { - font-size: 1.25rem; - } +.card-title { + font-weight: 600; + line-height: 1.3; + margin-right: 1rem; } + +.card-content { + font-weight: 300; +} + @media (min-width: 640px) { .blog-card { flex-direction: row; diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs index b932a383..84c1cedb 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Admin/DraftBlogPost/DraftBlogPostPageTests.cs @@ -27,6 +27,6 @@ public async Task ShouldOnlyShowPublishedPosts() var blogPosts = cut.FindComponents(); blogPosts.ShouldHaveSingleItem(); - blogPosts[0].Find(".description h1").InnerHtml.ShouldBe("Not published"); + blogPosts[0].Find(".description h4").InnerHtml.ShouldBe("Not published"); } } diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs index aac491c1..667b9567 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Bookmarks/BookmarksTests.cs @@ -33,7 +33,7 @@ public async Task ShouldOnlyDisplayBookmarkedPosts() var blogPosts = cut.FindComponents(); blogPosts.ShouldHaveSingleItem(); - blogPosts[0].Find(".description h1").TextContent.ShouldBe("Bookmarked Post"); + blogPosts[0].Find(".description h4").TextContent.ShouldBe("Bookmarked Post"); } [Fact] diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs index a5a64d29..3dbb7f5d 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs @@ -30,8 +30,8 @@ public async Task ShouldShowAllBlogPostsWithLatestOneFirst() var blogPosts = cut.FindComponents(); blogPosts.Count.ShouldBe(2); - blogPosts[0].Find(".description h1").InnerHtml.ShouldBe("New"); - blogPosts[1].Find(".description h1").InnerHtml.ShouldBe("Old"); + blogPosts[0].Find(".description h4").InnerHtml.ShouldBe("New"); + blogPosts[1].Find(".description h4").InnerHtml.ShouldBe("Old"); } [Fact] @@ -50,7 +50,7 @@ public async Task ShouldOnlyShowPublishedPosts() var blogPosts = cut.FindComponents(); blogPosts.ShouldHaveSingleItem(); - blogPosts[0].Find(".description h1").InnerHtml.ShouldBe("Published"); + blogPosts[0].Find(".description h4").InnerHtml.ShouldBe("Published"); } [Fact] diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs index 8813ba5f..1d085f13 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Search/SearchPageTests.cs @@ -23,7 +23,7 @@ public async Task ShouldFindBlogPostWhenTitleMatches() var cut = ctx.Render(p => p.Add(s => s.SearchTerm, "Title 1")); var blogPosts = cut.WaitForComponent(); - blogPosts.Find(".description h1").TextContent.ShouldBe("Title 1"); + blogPosts.Find(".description h4").TextContent.ShouldBe("Title 1"); } [Fact] @@ -39,7 +39,7 @@ public async Task ShouldFindBlogPostWhenTagMatches() var cut = ctx.Render(p => p.Add(s => s.SearchTerm, "Cat")); var blogPost = cut.WaitForComponent(); - blogPost.Find(".description h1").TextContent.ShouldBe("Title 1"); + blogPost.Find(".description h4").TextContent.ShouldBe("Title 1"); } [Fact] @@ -53,7 +53,7 @@ public async Task ShouldUnescapeQuery() var cut = ctx.Render(p => p.Add(s => s.SearchTerm, "Title%201")); var blogPosts = cut.WaitForComponent(); - blogPosts.Find(".description h1").TextContent.ShouldBe("Title 1"); + blogPosts.Find(".description h4").TextContent.ShouldBe("Title 1"); } private void RegisterServices(BunitContext ctx) From 21f7210bee85b4b3cc4e88a2ab5854c59019325f Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 26 Mar 2025 15:12:16 +0100 Subject: [PATCH 628/682] fix: Act on CryptographyException in LocalStorage (fixes #412) --- Directory.Packages.props | 4 +- .../Features/Bookmarks/BookmarkService.cs | 2 +- .../Home/Components/ThemeToggler.razor | 2 +- .../Features/Services/ILocalStorageService.cs | 2 +- .../Features/Services/LocalStorageService.cs | 49 +++++++++++++++++-- .../ShowBlogPost/Components/Like.razor | 8 +-- .../ShowBlogPost/ShowBlogPostPageTests.cs | 2 +- .../Home/Components/ThemeTogglerTests.cs | 2 +- .../ShowBlogPost/Components/LikeTests.cs | 4 +- 9 files changed, 57 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 796af21c..e9307145 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -52,6 +52,6 @@ - + - + \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs index 189fc28b..a84e6ab3 100644 --- a/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Bookmarks/BookmarkService.cs @@ -55,7 +55,7 @@ public async Task SetBookmark(string postId, bool isBookmarked) private async Task InitializeIfNotExists() { - if (!(await localStorageService.ContainKeyAsync("bookmarks"))) + if (!(await localStorageService.ContainsKeyAsync("bookmarks"))) { await localStorageService.SetItemAsync("bookmarks", new List()); } diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Components/ThemeToggler.razor b/src/LinkDotNet.Blog.Web/Features/Home/Components/ThemeToggler.razor index ab2f4686..735ee99c 100644 --- a/src/LinkDotNet.Blog.Web/Features/Home/Components/ThemeToggler.razor +++ b/src/LinkDotNet.Blog.Web/Features/Home/Components/ThemeToggler.razor @@ -51,7 +51,7 @@ { await using var _ = await JSRuntime.InvokeAsync("import", "./Features/Home/Components/ThemeToggler.razor.js"); - currentTheme = await LocalStorageService.ContainKeyAsync(StorageKey) + currentTheme = await LocalStorageService.ContainsKeyAsync(StorageKey) ? await LocalStorageService.GetItemAsync(StorageKey) : await JSRuntime.InvokeAsync("getCurrentSystemPreference"); diff --git a/src/LinkDotNet.Blog.Web/Features/Services/ILocalStorageService.cs b/src/LinkDotNet.Blog.Web/Features/Services/ILocalStorageService.cs index 1fd4bf40..ebc12372 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/ILocalStorageService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/ILocalStorageService.cs @@ -4,7 +4,7 @@ namespace LinkDotNet.Blog.Web.Features.Services; public interface ILocalStorageService { - ValueTask ContainKeyAsync(string key); + ValueTask ContainsKeyAsync(string key); ValueTask GetItemAsync(string key); diff --git a/src/LinkDotNet.Blog.Web/Features/Services/LocalStorageService.cs b/src/LinkDotNet.Blog.Web/Features/Services/LocalStorageService.cs index aa24ee8b..f71d7715 100644 --- a/src/LinkDotNet.Blog.Web/Features/Services/LocalStorageService.cs +++ b/src/LinkDotNet.Blog.Web/Features/Services/LocalStorageService.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; @@ -13,10 +15,47 @@ public LocalStorageService(ProtectedLocalStorage localStorage) this.localStorage = localStorage; } - public async ValueTask ContainKeyAsync(string key) => (await localStorage.GetAsync(key)).Success; + public async ValueTask ContainsKeyAsync(string key) + { + try + { + return (await localStorage.GetAsync(key)).Success; + } + catch (CryptographicException) + { + await localStorage.DeleteAsync(key); + return false; + } + } - public async ValueTask GetItemAsync(string key) => (await localStorage.GetAsync(key)).Value - ?? throw new KeyNotFoundException($"Key {key} not found"); + public async ValueTask GetItemAsync(string key) + { + try + { + var result = await localStorage.GetAsync(key); + if (!result.Success) + { + throw new KeyNotFoundException($"Key {key} not found"); + } + return result.Value!; + } + catch (CryptographicException) + { + await localStorage.DeleteAsync(key); + throw new KeyNotFoundException($"Key {key} was invalid and has been removed"); + } + } - public async ValueTask SetItemAsync(string key, T value) => await localStorage.SetAsync(key, value!); + public async ValueTask SetItemAsync(string key, T value) + { + try + { + await localStorage.SetAsync(key, value!); + } + catch (CryptographicException) + { + await localStorage.DeleteAsync(key); + throw new InvalidOperationException($"Could not set value for key {key}. The key has been removed."); + } + } } diff --git a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor index 2b69de55..448db8e2 100644 --- a/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor +++ b/src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/Like.razor @@ -3,7 +3,7 @@ @inject ILocalStorageService LocalStorage - - - -
    - - - The primary image which will be used. - -
    -
    - - - - Optional: Used as a fallback if the preview image can't be used by the browser. -
    For example using a jpg or png as fallback for avif which is not supported in Safari or Edge. -
    - -
    -
    - - - - If set the blog post will be published at the given date. - A blog post with a schedule date can't be set to published. - All dates are stored in UTC internally. - - -
    -
    - -
    - If this blog post is only draft or it will be scheduled, uncheck the box. - -
    -
    - - -
    - @if (BlogPost is not null && !IsScheduled) - { -
    - -
    - - If set the publish date is set to now, - otherwise its original date. - -
    - } -
    - -
    - The first page of the blog is cached. Therefore, the blog post is not immediately visible. - Head over to settings to invalidate the cache or enable the checkmark. -
    - The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page. -
    -
    - -
    +
    + + + + + +
    +
    +
    +
    +
    Content
    +
    +
    +
    +
    + + +
    + +
    + +
    + + + +
    + +
    + + + + +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    Quick Actions
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    Tags
    +
    +
    +
    + + +
    + + Add relevant tags to improve content discoverability and SEO. Use specific, searchable terms that describe your content. + + @if (!string.IsNullOrWhiteSpace(model.Tags)) + { +
    + @foreach (var tag in model.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + @tag.Trim() + } +
    + } +
    +
    + +
    +
    +
    Publishing & Visibility
    +
    +
    +
    + + + + @(model.IsPublished ? "Visible to everyone" : "Only visible to you") + +
    + + +
    + +
    + + +
    + @if (IsScheduled) + { +
    + Will be published on @model.ScheduledPublishDate?.ToString("MMMM dd, yyyy 'at' HH:mm") +
    + } + +
    + + @if (BlogPost is not null && !IsScheduled) + { +
    + + + + Update the post's publication date to now + +
    + } +
    +
    + +
    +
    +
    Featured Image
    +
    +
    +
    +
    + + +
    + +
    +
    +
    + + +
    + +
    + + The fallback image ensures compatibility across all platforms. Use JPG or PNG format for maximum support when your primary image is WebP or AVIF. + +
    +
    + +
    +
    +
    Cache Management
    +
    +
    +
    + + + + Make this post visible immediately on the homepage + +
    +
    + Enable this to make your post immediately visible on the homepage after publishing. +
    +
    +
    +
    +
    @@ -118,6 +248,7 @@ + @code { [Parameter] public BlogPost? BlogPost { get; set; } @@ -160,6 +291,34 @@ model = CreateNewModel.FromBlogPost(BlogPost); } + private string GetStatusText() + { + if (BlogPost != null) + { + return "Editing"; + } + + if (model.IsPublished) + { + return "Ready to Publish"; + } + + if (IsScheduled) + { + return "Scheduled"; + } + + return "Draft"; + } + + private int GetEstimatedReadTime() + { + if (string.IsNullOrWhiteSpace(model.Content)) + return 1; + + return ReadingTimeCalculator.CalculateReadingTime(model.Content); + } + private async Task OnValidBlogPostCreatedAsync() { canSubmit = false; diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs index 0a71e1ff..d574c320 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs @@ -67,8 +67,8 @@ public bool ShouldUpdateDate [FutureDateValidation] public DateTime? ScheduledPublishDate { - get => scheduledPublishDate?.ToLocalTime(); - set => SetProperty(out scheduledPublishDate, value?.ToUniversalTime()); + get => scheduledPublishDate; + set => SetProperty(out scheduledPublishDate, value); } public string Tags From 431978da2b5dc981ee4e03548c986ced3896fda3 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 15 Jun 2025 12:54:58 +0200 Subject: [PATCH 644/682] chore: Upgrade packages --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3a008294..57eabac5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + From 698922e23618515d1802b0a4a4fd948ee52c42aa Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Sun, 15 Jun 2025 17:35:10 +0200 Subject: [PATCH 645/682] Remove unused file --- .../BlogPostEditor/Components/CreateNewBlogPost.razor | 2 +- .../BlogPostEditor/Components/CreateNewBlogPost.razor.css | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor.css diff --git a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor index 3866cd45..8bc91ff4 100644 --- a/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor @@ -74,7 +74,7 @@
    -
    +