Compare commits

..

14 Commits

Author SHA1 Message Date
fa087a513d
style: write as first person 2024-09-17 20:46:49 +01:00
771ccc52ad
refactor: haha I'm a student now
dear god help me
2024-09-17 20:45:36 +01:00
59e42ff7cd
perf: use http utility to decode url 2024-07-15 19:44:02 +01:00
d32d46e221
fix: fix tag links 2024-07-15 19:38:56 +01:00
0bebcb69fe
fix: fix display of single-page tabs 2024-07-15 19:38:34 +01:00
d3ac89d071
refactor: use mastodon info from configuration 2024-07-05 17:56:42 +01:00
901a8347b9
refactor: move to new mastodon instance 2024-07-05 00:46:40 +01:00
8dbfeb8d38
chore: drop version in docker-compose 2024-06-16 19:29:37 +01:00
eb67c25e09
refactor: C != C++ 2024-06-16 19:16:48 +01:00
15e28bd223
feat: add keystroke class 2024-05-12 13:31:36 +01:00
acb6b32938
style: begin to organise app.scss monolith
colors now defined separately
2024-05-12 13:31:08 +01:00
cf4d92c035
fix: oops, page 2 was completely missing.
The model contains a 1-based page number, whereas GetBlogPosts wants 0-based index, causing an entire page to be missing.
2024-05-06 17:40:03 +01:00
58797b82ca
fix: don't include redirected posts in count 2024-05-06 17:39:28 +01:00
9991ecf173
Merge branch 'feature/cleanup' 2024-05-06 15:02:19 +01:00
14 changed files with 122 additions and 51 deletions

View File

@ -24,8 +24,9 @@ public interface IBlogPostService
/// Returns the total number of blog posts. /// Returns the total number of blog posts.
/// </summary> /// </summary>
/// <param name="visibility">The post visibility filter.</param> /// <param name="visibility">The post visibility filter.</param>
/// <param name="tags">The tags of the posts to return.</param>
/// <returns>The total number of blog posts.</returns> /// <returns>The total number of blog posts.</returns>
int GetBlogPostCount(Visibility visibility = Visibility.None); int GetBlogPostCount(Visibility visibility = Visibility.None, string[]? tags = null);
/// <summary> /// <summary>
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts /// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
@ -33,8 +34,9 @@ public interface IBlogPostService
/// </summary> /// </summary>
/// <param name="page">The zero-based index of the page to return.</param> /// <param name="page">The zero-based index of the page to return.</param>
/// <param name="pageSize">The maximum number of posts to return per page.</param> /// <param name="pageSize">The maximum number of posts to return per page.</param>
/// <param name="tags">The tags of the posts to return.</param>
/// <returns>A collection of blog posts.</returns> /// <returns>A collection of blog posts.</returns>
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10); IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10, string[]? tags = null);
/// <summary> /// <summary>
/// Returns the number of legacy comments for the specified post. /// Returns the number of legacy comments for the specified post.
@ -70,8 +72,9 @@ public interface IBlogPostService
/// </summary> /// </summary>
/// <param name="pageSize">The page size. Defaults to 10.</param> /// <param name="pageSize">The page size. Defaults to 10.</param>
/// <param name="visibility">The post visibility filter.</param> /// <param name="visibility">The post visibility filter.</param>
/// <param name="tags">The tags of the posts to return.</param>
/// <returns>The page count.</returns> /// <returns>The page count.</returns>
int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None); int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None, string[]? tags = null);
/// <summary> /// <summary>
/// Returns the previous blog post from the specified blog post. /// Returns the previous blog post from the specified blog post.

View File

@ -12,7 +12,7 @@
@await Html.PartialAsync("Partials/_MastodonStatus") @await Html.PartialAsync("Partials/_MastodonStatus")
<div id="all-blog-posts"> <div id="all-blog-posts">
@foreach (IBlogPost post in BlogPostService.GetBlogPosts(0)) @foreach (IBlogPost post in BlogPostService.GetBlogPosts(0, tags: Model.Tag))
{ {
@await Html.PartialAsync("Partials/_BlogCard", post) @await Html.PartialAsync("Partials/_BlogCard", post)
} }
@ -22,5 +22,6 @@
{ {
["UrlRoot"] = "/blog", ["UrlRoot"] = "/blog",
["Page"] = 1, ["Page"] = 1,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) ["Tags"] = Model.Tag,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published, tags: Model.Tag)
}) })

View File

@ -1,3 +1,4 @@
using System.Web;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Common.Data.Blog; using OliverBooth.Common.Data.Blog;
@ -15,11 +16,15 @@ public class Index : PageModel
_blogPostService = blogPostService; _blogPostService = blogPostService;
} }
public string[] Tag { get; private set; } = [];
public IActionResult OnGet([FromQuery(Name = "pid")] Guid? postId = null, public IActionResult OnGet([FromQuery(Name = "pid")] Guid? postId = null,
[FromQuery(Name = "p")] int? wpPostId = null) [FromQuery(Name = "p")] int? wpPostId = null,
[FromQuery(Name = "tag")] string? tag = null)
{ {
if (postId.HasValue == wpPostId.HasValue) if (postId.HasValue == wpPostId.HasValue)
{ {
Tag = (tag?.Split('+').Select(HttpUtility.UrlDecode).ToArray() ?? [])!;
return Page(); return Page();
} }

View File

@ -9,7 +9,7 @@
@await Html.PartialAsync("Partials/_MastodonStatus") @await Html.PartialAsync("Partials/_MastodonStatus")
<div id="all-blog-posts"> <div id="all-blog-posts">
@foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber)) @foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber - 1, tags: Model.Tag))
{ {
@await Html.PartialAsync("Partials/_BlogCard", post) @await Html.PartialAsync("Partials/_BlogCard", post)
} }
@ -19,5 +19,6 @@
{ {
["UrlRoot"] = "/blog", ["UrlRoot"] = "/blog",
["Page"] = Model.PageNumber, ["Page"] = Model.PageNumber,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) ["Tags"] = Model.Tag,
["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published, tags: Model.Tag)
}) })

View File

@ -1,3 +1,4 @@
using System.Web;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
@ -14,12 +15,15 @@ public class List : PageModel
/// <value>The requested page number.</value> /// <value>The requested page number.</value>
public int PageNumber { get; private set; } public int PageNumber { get; private set; }
public string[] Tag { get; private set; } = [];
/// <summary> /// <summary>
/// Handles the incoming GET request to the page. /// Handles the incoming GET request to the page.
/// </summary> /// </summary>
/// <param name="page">The requested page number, starting from 1.</param> /// <param name="page">The requested page number, starting from 1.</param>
/// <param name="tag">The tag by which to filter results.</param>
/// <returns></returns> /// <returns></returns>
public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1) public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1, [FromQuery(Name = "tag")] string? tag = null)
{ {
if (page < 2) if (page < 2)
{ {
@ -27,6 +31,7 @@ public class List : PageModel
} }
PageNumber = page; PageNumber = page;
Tag = (tag?.Split('+').Select(HttpUtility.UrlDecode).ToArray() ?? [])!;
return Page(); return Page();
} }
} }

View File

@ -15,11 +15,11 @@
</div> </div>
<p> <p>
My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++, I recently became a Computer Science student, aiming to get my bachelor's degree. My primary focus is C#, though
Python, and others. Over the years I've built up a collection of projects. Some of which I'm extremely proud of, I have dabbled in several other languages such as Java, Kotlin, VB, C++, Python, and others. Over the years I've
and others I've quietly abandoned and buried. I'm currently working on a few projects that I hope to release in built up a collection of projects. Some of which I'm extremely proud of, and others I've quietly abandoned and
the near future, but in the meantime, feel free to check out some of my buried. I'm currently working on a few projects that I hope to release in the near future, but in the meantime,
<a asp-page="/Projects/Index">previous work</a>. feel free to check out some of my <a asp-page="/Projects/Index">previous work</a>.
</p> </p>
<p> <p>

View File

@ -32,6 +32,8 @@ public class PageTabsUtility
set => _urlRoot = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); set => _urlRoot = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
} }
public string[]? Tags { get; set; } = [];
/// <summary> /// <summary>
/// Shows the bound chevrons for the specified bounds type. /// Shows the bound chevrons for the specified bounds type.
/// </summary> /// </summary>
@ -129,7 +131,9 @@ public class PageTabsUtility
private string GetLinkForPage(int page) private string GetLinkForPage(int page)
{ {
// page 1 doesn't use /page/n route // page 1 doesn't use /page/n route
return page == 1 ? _urlRoot : $"{_urlRoot}/page/{page}"; return page == 1
? $"{_urlRoot}{(Tags is { Length: > 0 } ? $"?tag={string.Join('+', Tags)}" : "")}"
: $"{_urlRoot}/page/{page}{(Tags is { Length: > 0 } ? $"?tag={string.Join('+', Tags)}" : "")}";
} }
private string ShowLowerBound() private string ShowLowerBound()

View File

@ -7,15 +7,23 @@
{ {
CurrentPage = page, CurrentPage = page,
PageCount = pageCount, PageCount = pageCount,
UrlRoot = urlRoot UrlRoot = urlRoot,
Tags = ViewData["Tags"] as string[]
}; };
} }
<nav> <nav>
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
@Html.Raw(utility.ShowBounds(PageTabsUtility.BoundsType.Lower)) @if (pageCount == 1)
@Html.Raw(utility.ShowTab(1)) @* always visible *@ {
@Html.Raw(utility.ShowTabWindow()) @Html.Raw(utility.ShowTab(1)) @* always visible *@
@Html.Raw(utility.ShowTab(pageCount)) @* always visible *@ }
@Html.Raw(utility.ShowBounds(PageTabsUtility.BoundsType.Upper)) else
{
@Html.Raw(utility.ShowBounds(PageTabsUtility.BoundsType.Lower))
@Html.Raw(utility.ShowTab(1)) @* always visible *@
@Html.Raw(utility.ShowTabWindow())
@Html.Raw(utility.ShowTab(pageCount)) @* always visible *@
@Html.Raw(utility.ShowBounds(PageTabsUtility.BoundsType.Upper))
}
</ul> </ul>
</nav> </nav>

View File

@ -5,6 +5,7 @@
@using OliverBooth.Extensions @using OliverBooth.Extensions
@inject IBlogPostService BlogPostService @inject IBlogPostService BlogPostService
@inject ITutorialService TutorialService @inject ITutorialService TutorialService
@inject IConfiguration Configuration
@{ @{
HttpRequest request = Context.Request; HttpRequest request = Context.Request;
var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}"); var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}");
@ -106,7 +107,11 @@
<hr> <hr>
<ul class="footer-nav"> <ul class="footer-nav">
<li><a title="@("@oliver@mastodon.olivr.me")" href="https://mastodon.olivr.me/@@oliver" rel="me" class="brand-mastodon"><i class="fa-brands fa-mastodon"></i></a></li> @{
string domain = Configuration.GetSection("Mastodon:Domain").Value ?? string.Empty;
string username = Configuration.GetSection("Mastodon:Username").Value ?? string.Empty;
}
<li><a title="@($"@{username}@{domain}")" href="https://@domain/@@@username" rel="me" class="brand-mastodon"><i class="fa-brands fa-mastodon"></i></a></li>
<li><a title="LinkedIn/oliverlukebooth" href="https://www.linkedin.com/in/oliverlukebooth/" class="brand-linkedin"><i class="fa-brands fa-linkedin"></i></a></li> <li><a title="LinkedIn/oliverlukebooth" href="https://www.linkedin.com/in/oliverlukebooth/" class="brand-linkedin"><i class="fa-brands fa-linkedin"></i></a></li>
<li><a title="Blog RSS Feed" asp-controller="Rss" asp-action="OnGet"><i class="fa-solid fa-rss text-orange"></i></a></li> <li><a title="Blog RSS Feed" asp-controller="Rss" asp-action="OnGet"><i class="fa-solid fa-rss text-orange"></i></a></li>
<li><a title="View Source" href="https://git.oliverbooth.dev/oliverbooth/oliverbooth.dev"><i class="fa-solid fa-code"></i></a></li> <li><a title="View Source" href="https://git.oliverbooth.dev/oliverbooth/oliverbooth.dev"><i class="fa-solid fa-code"></i></a></li>

View File

@ -36,12 +36,19 @@ internal sealed class BlogPostService : IBlogPostService
} }
/// <inheritdoc /> /// <inheritdoc />
public int GetBlogPostCount(Visibility visibility = Visibility.None) public int GetBlogPostCount(Visibility visibility = Visibility.None, string[]? tags = null)
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
if (tags is { Length: > 0 })
{
return visibility == Visibility.None
? context.BlogPosts.AsEnumerable().Count(p => !p.IsRedirect && p.Tags.Intersect(tags).Any())
: context.BlogPosts.AsEnumerable().Count(p => !p.IsRedirect && p.Visibility == visibility && p.Tags.Intersect(tags).Any());
}
return visibility == Visibility.None return visibility == Visibility.None
? context.BlogPosts.Count() ? context.BlogPosts.Count(p => !p.IsRedirect)
: context.BlogPosts.Count(p => p.Visibility == visibility); : context.BlogPosts.Count(p => !p.IsRedirect && p.Visibility == visibility);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -60,13 +67,20 @@ internal sealed class BlogPostService : IBlogPostService
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10) public IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10, string[]? tags = null)
{ {
using BlogContext context = _dbContextFactory.CreateDbContext(); using BlogContext context = _dbContextFactory.CreateDbContext();
return context.BlogPosts IEnumerable<BlogPost> posts = context.BlogPosts
.Where(p => p.Visibility == Visibility.Published && !p.IsRedirect) .Where(p => p.Visibility == Visibility.Published && !p.IsRedirect)
.OrderByDescending(post => post.Published) .OrderByDescending(post => post.Published)
.Skip(page * pageSize) .AsEnumerable();
if (tags is { Length: > 0 })
{
posts = posts.Where(p => p.Tags.Intersect(tags).Any());
}
return posts.Skip(page * pageSize)
.Take(pageSize) .Take(pageSize)
.ToArray().Select(CacheAuthor).ToArray(); .ToArray().Select(CacheAuthor).ToArray();
} }
@ -103,9 +117,9 @@ internal sealed class BlogPostService : IBlogPostService
} }
/// <inheritdoc /> /// <inheritdoc />
public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None) public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None, string[]? tags = null)
{ {
float postCount = GetBlogPostCount(visibility); float postCount = GetBlogPostCount(visibility, tags);
return (int)MathF.Ceiling(postCount / pageSize); return (int)MathF.Ceiling(postCount / pageSize);
} }

View File

@ -27,11 +27,13 @@ internal sealed class MastodonService : IMastodonService
/// <inheritdoc /> /// <inheritdoc />
public IMastodonStatus GetLatestStatus() public IMastodonStatus GetLatestStatus()
{ {
string domain = _configuration.GetSection("Mastodon:Domain").Value ?? string.Empty;
string token = _configuration.GetSection("Mastodon:Token").Value ?? string.Empty; string token = _configuration.GetSection("Mastodon:Token").Value ?? string.Empty;
string account = _configuration.GetSection("Mastodon:Account").Value ?? string.Empty; string account = _configuration.GetSection("Mastodon:Account").Value ?? string.Empty;
using var request = new HttpRequestMessage(); using var request = new HttpRequestMessage();
request.Headers.Add("Authorization", $"Bearer {token}"); request.Headers.Add("Authorization", $"Bearer {token}");
request.RequestUri = new Uri($"https://mastodon.olivr.me/api/v1/accounts/{account}/statuses"); request.RequestUri = new Uri($"https://{domain}/api/v1/accounts/{account}/statuses");
using HttpResponseMessage response = _httpClient.Send(request); using HttpResponseMessage response = _httpClient.Send(request);
using var stream = response.Content.ReadAsStream(); using var stream = response.Content.ReadAsStream();

View File

@ -1,4 +1,3 @@
version: '3.9'
services: services:
oliverbooth: oliverbooth:
container_name: oliverbooth.dev container_name: oliverbooth.dev

View File

@ -1,9 +1,11 @@
@use "sass:color";
@import "colors";
@import "markdown"; @import "markdown";
@import "blog"; @import "blog";
html, body { html, body {
background: #121212; background: $background;
color: #f5f5f5; color: $foreground;
font-size: 16px; font-size: 16px;
} }
@ -13,8 +15,18 @@ html, body {
} }
} }
.keystroke {
background: color.adjust($foreground, $lightness: -20%);
color: $background;
border-radius: 2px;
border: color.adjust($foreground, $lightness: -30%);
box-shadow: #2b2b2b 2px 2px;
font-size: 12px;
padding: 2px 4px;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; box-shadow: 0 0 0 0.1rem $ui-foreground, 0 0 0 0.25rem #258cfb;
} }
html { html {
@ -35,11 +47,11 @@ main.container {
a { a {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
text-decoration: none; text-decoration: none;
color: #03A9F4; color: $link-text;
} }
&:hover { &:hover {
color: #ffffff; color: $link-hover;
} }
} }
@ -47,19 +59,19 @@ a {
a { a {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
text-decoration: none; text-decoration: none;
color: white; color: $ui-foreground;
* { * {
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
text-decoration: none; text-decoration: none;
color: white; color: $ui-foreground;
} }
} }
} }
} }
hr.page-separator { hr.page-separator {
border-top: 5px dashed #ffffff; border-top: 5px dashed $ui-foreground;
opacity: 1; opacity: 1;
margin: 50px 0; margin: 50px 0;
} }
@ -86,7 +98,7 @@ ul.contact-reasons {
a { a {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
text-decoration: none; text-decoration: none;
color: white; color: $ui-foreground;
} }
&:hover { &:hover {
@ -98,10 +110,10 @@ ul.contact-reasons {
.card { .card {
background: #212121; background: #212121;
color: #ffffff; color: $ui-foreground;
a.btn, button { a.btn, button {
color: #ffffff; color: $ui-foreground;
} }
} }
@ -128,7 +140,7 @@ nav {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
text-decoration: none; text-decoration: none;
color: white; color: $ui-foreground;
} }
&:hover { &:hover {
@ -144,7 +156,7 @@ nav {
} }
article { article {
background: #333333; background: $content-background;
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;
@ -153,7 +165,7 @@ article {
} }
blockquote { blockquote {
border-left: 2px solid #f03; border-left: 2px solid $blockquote;
padding-left: 10px; padding-left: 10px;
} }
@ -173,7 +185,7 @@ article {
abbr { abbr {
text-decoration: none; text-decoration: none;
border-bottom: 1px dotted #ffffff; border-bottom: 1px dotted $link-hover;
} }
span.timestamp { span.timestamp {
@ -185,7 +197,7 @@ article {
.project-card { .project-card {
position: relative; position: relative;
background: #000; background: $project-background;
box-shadow: 0 0 15px rgba(0, 0, 0, .1); box-shadow: 0 0 15px rgba(0, 0, 0, .1);
&:hover { &:hover {
@ -213,7 +225,7 @@ article {
a { a {
&:link, &:visited, &:hover &:active { &:link, &:visited, &:hover &:active {
color: #fff; color: $ui-foreground;
} }
} }
@ -225,7 +237,7 @@ article {
width: 100%; width: 100%;
margin: -34px 0 0; margin: -34px 0 0;
padding: 5px; padding: 5px;
background-color: rgba(#000, 50%); background-color: rgba($project-background, 50%);
} }
} }
} }
@ -414,7 +426,7 @@ td.trim-p p:last-child {
} }
&:hover { &:hover {
color: #03A9F4; color: $accent;
background-color: #1E1E1E !important; background-color: #1E1E1E !important;
} }
} }

12
src/scss/colors.scss Normal file
View File

@ -0,0 +1,12 @@
@use "sass:color";
$background: #121212;
$foreground: #f5f5f5;
$accent: #03a9f4;
$ui-foreground: #ffffff;
$content-background: #333333;
$blockquote: #ff0033;
$project-background: #000000;
$link-text: $accent;
$link-hover: lighten($link-text, 50%);