Author SHA1 Message Date
Josh-Heaps 52d468a46e Update blog css 2026-03-07 12:26:58 -07:00
Josh-Heaps ea60ee5a4b fix css 2026-03-06 17:15:51 -07:00
Josh-Heaps 99a9474ef7 Add cache invalidation 2026-03-06 17:05:48 -07:00
Josh-HeapsandClaude Opus 4.6 8043eee4cc Replace file-based blog with API-backed service
BlogService now fetches from Media.JoshHeaps.Net API via HttpClient
with in-memory caching (5-min TTL) and stale cache fallback.
Removed Markdig/YamlDotNet dependencies. All page models now async.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-06 15:33:51 -07:00
Josh Heaps 5fd3a5d7c1 Merge pull request #12 from JoshHeaps/AddProject
Add cloud storage to projects section of front page
2025-10-22 11:58:33 -06:00
26 changed files with 1184 additions and 238 deletions
+3 -1
View File
@@ -3,7 +3,9 @@
"allow": [ "allow": [
"Bash(dotnet test:*)", "Bash(dotnet test:*)",
"Bash(dotnet build)", "Bash(dotnet build)",
"Bash(dir:*)" "Bash(dir:*)",
"Bash(dotnet add:*)",
"Bash(dotnet build:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
@@ -0,0 +1,24 @@
---
title: My First Post
date: 2026-03-06
summary: Welcome to my blog! Here I'll share thoughts on software development, projects I'm working on, and things I find interesting.
tags: [dev, personal]
---
# Welcome to My Blog
I've been meaning to start writing about the things I build and learn, and I'm finally getting around to it.
## What to Expect
I plan to write about:
- **Projects** I'm working on, like this website and the other things on my portfolio
- **Software development** tips and patterns I find useful
- **Problem solving** approaches that have helped me grow as a developer
## Why a Blog?
Building things is great, but explaining *how* and *why* you built them is just as valuable. Writing forces you to organize your thoughts, and hopefully someone else finds it useful along the way.
Stay tuned for more posts!
@@ -0,0 +1,20 @@
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace JoshHeaps.Net.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BlogController(IBlogService blogService, IConfiguration configuration) : ControllerBase
{
[HttpPost("invalidate")]
public IActionResult InvalidateCache([FromHeader(Name = "X-Invalidate-Key")] string? key)
{
var expectedKey = configuration["BlogApi:InvalidateKey"];
if (string.IsNullOrEmpty(expectedKey) || key != expectedKey)
return Unauthorized();
blogService.ClearCache();
return Ok();
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace JoshHeaps.Net.Models;
public class BlogPost
{
public long Id { get; set; }
public string Slug { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public DateTime PublishedDate { get; set; }
public string Summary { get; set; } = string.Empty;
public List<string> Tags { get; set; } = [];
public string HtmlContent { get; set; } = string.Empty;
}
+49
View File
@@ -0,0 +1,49 @@
@page
@model JoshHeaps.Net.Pages.BlogModel
@{
ViewData["Title"] = "Blog - JoshHeaps.Net";
Layout = "_Layout";
}
<div class="blog-index">
<h1 class="blog-title">Blog</h1>
@if (Model.ActiveTag is not null)
{
<div class="blog-filter">
<span>Filtered by: <strong>@Model.ActiveTag</strong></span>
<a href="/blog">Clear filter</a>
</div>
}
@if (Model.Posts.Count == 0)
{
<p class="blog-empty">No posts yet. Check back soon!</p>
}
<div class="blog-list">
@foreach (var post in Model.Posts)
{
<article class="blog-card">
<a href="/blog/@post.Slug" class="blog-card-link">
<h2 class="blog-card-title">@post.Title</h2>
</a>
<time class="blog-card-date" datetime="@post.PublishedDate.ToString("yyyy-MM-dd")">
@post.PublishedDate.ToString("MMMM d, yyyy")
</time>
<p class="blog-card-summary">@post.Summary</p>
<div class="blog-card-tags">
@foreach (var tag in post.Tags)
{
<a href="/blog?tag=@tag" class="blog-tag">@tag</a>
}
</div>
</article>
}
</div>
</div>
@section Styles {
<link href="/css/variables.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/blog/index.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
}
+26
View File
@@ -0,0 +1,26 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages;
public class BlogModel : PageModel
{
private readonly IBlogService _blogService;
public List<BlogPost> Posts { get; set; } = [];
public string? ActiveTag { get; set; }
public BlogModel(IBlogService blogService)
{
_blogService = blogService;
}
public async Task OnGetAsync(string? tag)
{
ActiveTag = tag;
Posts = tag is not null
? await _blogService.GetPostsByTagAsync(tag)
: await _blogService.GetAllPostsAsync();
}
}
+38
View File
@@ -0,0 +1,38 @@
@page "/blog/{slug}"
@model JoshHeaps.Net.Pages.Blog.PostModel
@{
ViewData["Title"] = Model.Post!.Title + " - JoshHeaps.Net";
Layout = "_Layout";
}
<article class="blog-post">
<a href="/blog" class="blog-back">&larr; Back to blog</a>
<header class="blog-post-header">
<h1 class="blog-post-title">@Model.Post.Title</h1>
<time class="blog-post-date" datetime="@Model.Post.PublishedDate.ToString("yyyy-MM-dd")">
@Model.Post.PublishedDate.ToString("MMMM d, yyyy")
</time>
<div class="blog-post-tags">
@foreach (var tag in Model.Post.Tags)
{
<a href="/blog?tag=@tag" class="blog-tag">@tag</a>
}
</div>
</header>
<div class="post-content">
@Html.Raw(Model.Post.HtmlContent)
</div>
</article>
@section Styles {
<link href="/css/variables.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/blog/post.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/blog/prism-tomorrow.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
}
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
}
+28
View File
@@ -0,0 +1,28 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages.Blog;
public class PostModel : PageModel
{
private readonly IBlogService _blogService;
public BlogPost? Post { get; set; }
public PostModel(IBlogService blogService)
{
_blogService = blogService;
}
public async Task<IActionResult> OnGetAsync(string slug)
{
Post = await _blogService.GetPostBySlugAsync(slug);
if (Post is null)
return NotFound();
return Page();
}
}
+72 -72
View File
@@ -1,115 +1,115 @@
@page @page
@model JoshHeaps.Net.Pages.IndexModel @model JoshHeaps.Net.Pages.IndexModel
@{ @{
ViewData["Title"] = "JoshHeaps.Net"; ViewData["Title"] = "JoshHeaps.Net";
Layout = "_Layout"; Layout = "_Layout";
} }
<header id="Header"> <nav id="Header">
<a class="headerOption" onclick="showClickedContents('projects')">./ Projects</a> <span class="site-name">josh heaps</span>
<a class="headerOption" onclick="showClickedContents('demos')">./ Demos</a> <div class="nav-links">
<a class="headerOption" onclick="showClickedContents('contact')">./ Contact me</a> <a class="nav-link" onclick="showClickedContents('projects')">Projects</a>
</header> <a class="nav-link" onclick="showClickedContents('demos')">Demos</a>
<a class="nav-link" onclick="showClickedContents('blog')">Blog</a>
<a class="nav-link" onclick="showClickedContents('contact')">Contact</a>
</div>
</nav>
<div id="WelcomeMessage"></div> <section id="Hero">
<div id="WelcomeMessage"></div>
</section>
<div id="ProjectsBox"> <section id="ProjectsBox">
<div id="ChessProject" class="projectContainer"> <h2 class="section-title">Projects</h2>
<div class="displayBox diagonal-section-left"> <div class="projects-grid">
<a target="_blank" href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml"> <article class="project-card">
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/> <a href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml" target="_blank" class="project-image-link">
<img class="projectImage" src="/images/Chess.jpg" alt="Chessboard" />
</a> </a>
<div class="project-info">
<h3 class="project-title">Chess</h3>
<p class="project-description">As someone who loves chess, I wanted to create a chess game that I could play with my friends and family. It was made using .NET razor pages, a .NET web api backend, and a good amount of javascript on the frontend. Feel free to check it out in the demos below :)</p>
<a class="project-link" href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml" target="_blank">View on GitHub &rarr;</a>
</div> </div>
<div id="ChessText" class="projectTextRight"> </article>
<h2 id="ChessTitle" class="projectHeaderRight projectHeader">Chess</h2>
<p id="ChessDescription" class="projectDescriptionRight projectDescription">
As someone who loves chess, I wanted to create a chess game that I could play with my friends and family. It was made using .NET razor pages, a .NET web api backend, and a good amount of javascript on the frontend. Feel free to check it out in the demos below :)
</p>
</div>
</div>
<div id="CompilerProject" class="projectContainer"> <article class="project-card">
<div id="CompilerText" class="projectTextLeft"> <a href="https://github.com/JoshHeaps/CILCompiler" target="_blank" class="project-image-link">
<h2 id="CompilerTitle" class="projectHeaderLeft projectHeader">Compiler</h2> <img class="projectImage" src="/images/CompilerDemo.png" alt="Compiler Input and Output" />
<p id="CompilerDescription" class="projectDescriptionLeft projectDescription">
I love solving problems, and figuring out how things work. What better way to do both than writing a compiler myself? This project is a compiler that I wrote in C# on .NET 8.0. It takes in a simple language that I created, outputs the corresponding common intermediate language (CIL), and executes it. I plan to add more features in the future.
</p>
</div>
<div class="displayBox diagonal-section-right">
<a target="_blank" href="https://github.com/JoshHeaps/CILCompiler">
<img id="CompilerImage" class="projectImage projectImageRight" src="/images/CompilerDemo.png" title="Compiler Input and Output"/>
</a> </a>
<div class="project-info">
<h3 class="project-title">Compiler</h3>
<p class="project-description">I love solving problems, and figuring out how things work. What better way to do both than writing a compiler myself? This project is a compiler that I wrote in C# on .NET 8.0. It takes in a simple language that I created, outputs the corresponding common intermediate language (CIL), and executes it. I plan to add more features in the future.</p>
<a class="project-link" href="https://github.com/JoshHeaps/CILCompiler" target="_blank">View on GitHub &rarr;</a>
</div> </div>
</div> </article>
<div id="CloudStorageProject" class="projectContainer"> <article class="project-card">
<div class="displayBox diagonal-section-left"> <a href="https://media.joshheaps.net" target="_blank" class="project-image-link">
<a target="_blank" href="https://media.joshheaps.net"> <img class="projectImage" src="/images/CloudStorageDemo.png" alt="Cloud Storage Screenshot" />
<img id="CloudStorageImage" class="projectImage projectImageLeft" src="/images/CloudStorageDemo.png" title="CloudStorageScreenshot" />
</a> </a>
<div class="project-info">
<h3 class="project-title">Image Cloud Storage</h3>
<p class="project-description">My wife and I LOVE to take pictures and videos of our kids, but we do not like paying for extra cloud storage. I made this for my family so we can upload pictures to my server, share them with each other, and now we don't need to worry about how much space we're using.</p>
<a class="project-link" href="https://media.joshheaps.net" target="_blank">View Live Demo &rarr;</a>
</div> </div>
<div id="CloudStorageText" class="projectTextRight"> </article>
<h2 id="CloudStorageTitle" class="projectHeaderRight projectHeader">Image Cloud Storage</h2>
<p id="CloudStorageDescription" class="projectDescriptionRight projectDescription">
My wife and I LOVE to take pictures and videos of our kids, but we do not like paying for extra cloud storage. I made this for my family so we can upload pictures to my server, share them with each other, and now we don't need to worry about how much space we're using.
</p>
</div> </div>
</div> </section>
</div>
<div id="DemosBox" class="displayBox"> <section id="DemosBox">
<h2 id="DemoHeader">Demos</h2> <h2 class="section-title">Demos</h2>
<div id="buttonWrapper"> <div id="buttonWrapper">
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/chess'"> <button class="demoButton" onclick="window.location.href='/chess'">Play Chess</button>
Play Chess <button class="demoButton" onclick="window.location.href='/particles'">Particle Simulator</button>
</button> <button class="demoButton" onclick="window.location.href='/memorylane'">Memory Lane</button>
<button id="ParticleDemo" class="demoButton" onclick="window.location.href='/particles'"> <button class="demoButton" onclick="window.location.href='https://media.joshheaps.net'">Cloud Image Storage</button>
Particle Simulator <button id="MoreFiller" class="demoButton">More coming soon...</button>
</button>
<button id="MemoryLane" class="demoButton" onclick="window.location.href='/memorylane'">
Memory Lane
</button>
<button id="CloadStorageDemo" class="demoButton" onclick="window.location.href='https://media.joshheaps.net'">
Cloud Image Storage
</button>
<button id="MoreFiller" class="demoButton">
More coming soon...
</button>
</div> </div>
</div> </section>
<section id="BlogBox">
<h2 class="section-title">Latest Posts</h2>
<div id="blogPostsList">
@foreach (var post in Model.LatestPosts)
{
<div class="blog-teaser-item">
<a class="blog-teaser-title" href="/blog/@post.Slug">@post.Title</a>
<span class="blog-teaser-date">@post.PublishedDate.ToString("MMMM d, yyyy")</span>
<p class="blog-teaser-summary">@post.Summary</p>
</div>
}
</div>
<a class="blog-view-all" href="/blog">View all posts &rarr;</a>
</section>
<footer id="Contacts"> <footer id="Contacts">
<a class="social-link" href="https://github.com/JoshHeaps" target="_blank">
<svg viewBox="0 0 16 16" class="social-icon"> <svg viewBox="0 0 16 16" class="social-icon">
<a id="GithubLink" class="social-link" href="https://github.com/JoshHeaps" target="_blank">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
<rect width="100%" height="100%" fill="transparent" />
</a>
</svg> </svg>
</a>
<a class="social-link" href="https://www.linkedin.com/in/josh-heaps/" target="_blank">
<svg viewBox="0 0 24 24" class="social-icon"> <svg viewBox="0 0 24 24" class="social-icon">
<a id="LinkedinLink" class="social-link" href="https://www.linkedin.com/in/josh-heaps/" target="_blank">
<path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"></path> <path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"></path>
<rect width="100%" height="100%" fill="transparent" />
</a>
</svg> </svg>
</a>
<a class="social-link" href="mailto:[email protected]">
<svg viewBox="0 -4 31 31" class="social-icon" preserveAspectRatio="none"> <svg viewBox="0 -4 31 31" class="social-icon" preserveAspectRatio="none">
<a id="GmailLink" class="social-link" href="mailto:[email protected]">
<path d="m0 3.23863636v2.72727273l3.12784091 3.02727273 3.69034091 2.08636368.68181818-4.59034095-.68181818-4.27329546-1.90909091-1.43181818c-2.02329546-1.51704546-4.90909091-.07329545-4.90909091 2.45454545"/> <path d="m0 3.23863636v2.72727273l3.12784091 3.02727273 3.69034091 2.08636368.68181818-4.59034095-.68181818-4.27329546-1.90909091-1.43181818c-2.02329546-1.51704546-4.90909091-.07329545-4.90909091 2.45454545"/>
<path d="m23.1818182 2.21590909-.6818182 4.32954546.6818182 4.53409095 3.3494318-1.65852277 3.46875-3.45511364v-2.72727273c0-2.5278409-2.8857955-3.97159091-4.9090909-2.45454545z"/> <path d="m23.1818182 2.21590909-.6818182 4.32954546.6818182 4.53409095 3.3494318-1.65852277 3.46875-3.45511364v-2.72727273c0-2.5278409-2.8857955-3.97159091-4.9090909-2.45454545z"/>
<path d="m2.04545455 22.6704545h4.77272727v-11.590909l-6.81818182-5.11363641v14.65909091c0 1.1301136.91534091 2.0454545 2.04545455 2.0454545"/> <path d="m2.04545455 22.6704545h4.77272727v-11.590909l-6.81818182-5.11363641v14.65909091c0 1.1301136.91534091 2.0454545 2.04545455 2.0454545"/>
<path d="m23.1818182 22.6704545h4.7727273c1.1301136 0 2.0454545-.9153409 2.0454545-2.0454545v-14.65909091l-6.8181818 5.11363641z"/> <path d="m23.1818182 22.6704545h4.7727273c1.1301136 0 2.0454545-.9153409 2.0454545-2.0454545v-14.65909091l-6.8181818 5.11363641z"/>
<path d="m15 8.35227273-8.18181818-6.13636364v8.86363641l8.18181818 6.1363636 8.1818182-6.1363636v-8.86363641z"/> <path d="m15 8.35227273-8.18181818-6.13636364v8.86363641l8.18181818 6.1363636 8.1818182-6.1363636v-8.86363641z"/>
<rect width="100%" height="100%" fill="transparent" />
</a>
</svg> </svg>
</a>
</footer> </footer>
@section Styles { @section Styles {
<link href="/css/variables.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/site.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" /> <link href="/css/home/site.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/project.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" /> <link href="/css/home/project.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/imageLeftProject.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" /> <link href="/css/home/blog.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/imageRightProject.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/home/footer.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" /> <link href="/css/home/footer.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
} }
+8 -3
View File
@@ -1,12 +1,17 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages namespace JoshHeaps.Net.Pages
{ {
public class IndexModel() : PageModel public class IndexModel(IBlogService blogService) : PageModel
{
public void OnGet()
{ {
public List<BlogPost> LatestPosts { get; set; } = [];
public async Task OnGetAsync()
{
var allPosts = await blogService.GetAllPostsAsync();
LatestPosts = allPosts.Take(3).ToList();
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
@{ @{
Layout = null; Layout = null;
ViewData["cssVersion"] = "1.0.2"; // <--- change this once to bust cache ViewData["cssVersion"] = "1.0.5"; // <--- change this once to bust cache
} }
<!DOCTYPE html> <!DOCTYPE html>
+7
View File
@@ -12,6 +12,13 @@ builder.Services.AddControllers();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
builder.Services.AddHttpClient("BlogApi", client =>
{
var baseUrl = configuration["BlogApi:BaseUrl"] ?? "https://media.joshheaps.net";
client.BaseAddress = new Uri(baseUrl);
client.Timeout = TimeSpan.FromSeconds(10);
});
builder.Services.AddSingleton<IBlogService, BlogService>();
builder.Services.AddSingleton<IChessService, ChessService>(); builder.Services.AddSingleton<IChessService, ChessService>();
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
@@ -0,0 +1,94 @@
using System.Collections.Concurrent;
using System.Text.Json;
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
namespace JoshHeaps.Net.Services.Implementations;
public class BlogService : IBlogService
{
private readonly HttpClient _httpClient;
private readonly ILogger<BlogService> _logger;
private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(5);
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public BlogService(IHttpClientFactory httpClientFactory, ILogger<BlogService> logger)
{
_httpClient = httpClientFactory.CreateClient("BlogApi");
_logger = logger;
}
public async Task<List<BlogPost>> GetAllPostsAsync()
{
return await GetCachedAsync("all_posts",
() => FetchAsync<List<BlogPost>>("api/blog/posts")) ?? [];
}
public async Task<BlogPost?> GetPostBySlugAsync(string slug)
{
return await GetCachedAsync($"post_{slug}",
() => FetchAsync<BlogPost>($"api/blog/posts/{Uri.EscapeDataString(slug)}"));
}
public async Task<List<BlogPost>> GetPostsByTagAsync(string tag)
{
return await GetCachedAsync($"tag_{tag}",
() => FetchAsync<List<BlogPost>>($"api/blog/posts/tags/{Uri.EscapeDataString(tag)}")) ?? [];
}
private async Task<T?> FetchAsync<T>(string path) where T : class
{
try
{
var response = await _httpClient.GetAsync(path);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Blog API returned {StatusCode} for {Path}", response.StatusCode, path);
return null;
}
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json, JsonOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch from blog API: {Path}", path);
return null;
}
}
private async Task<T?> GetCachedAsync<T>(string key, Func<Task<T?>> factory) where T : class
{
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTime.UtcNow)
return (T?)entry.Value;
var result = await factory();
if (result is not null)
{
_cache[key] = new CacheEntry(result, DateTime.UtcNow.Add(_cacheTtl));
}
else if (entry is not null)
{
// API unreachable — serve stale cache
_logger.LogWarning("Serving stale cache for key {Key}", key);
return (T?)entry.Value;
}
return result;
}
public void ClearCache()
{
_cache.Clear();
_logger.LogInformation("Blog cache cleared");
}
private record CacheEntry(object Value, DateTime ExpiresAt);
}
@@ -0,0 +1,11 @@
using JoshHeaps.Net.Models;
namespace JoshHeaps.Net.Services.Interfaces;
public interface IBlogService
{
Task<List<BlogPost>> GetAllPostsAsync();
Task<BlogPost?> GetPostBySlugAsync(string slug);
Task<List<BlogPost>> GetPostsByTagAsync(string tag);
void ClearCache();
}
+5 -1
View File
@@ -5,5 +5,9 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"BlogApi": {
"BaseUrl": "https://media.joshheaps.net",
"InvalidateKey": "CHANGE_ME"
}
} }
+126
View File
@@ -0,0 +1,126 @@
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-text);
overflow-x: hidden;
font-family: var(--font-body);
line-height: 1.6;
}
.blog-index {
max-width: 800px;
margin: 0 auto;
padding: 4rem clamp(1.5rem, 5vw, 3rem);
}
.blog-title {
color: var(--color-heading);
font-size: clamp(1.75rem, 3vw, 2.25rem);
font-weight: 700;
letter-spacing: -0.02em;
margin: 0 0 2.5rem;
}
.blog-filter {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
color: var(--color-text-muted);
font-size: 0.875rem;
}
.blog-filter strong {
color: var(--color-heading-secondary);
}
.blog-filter a {
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s;
}
.blog-filter a:hover {
color: var(--color-accent-hover);
}
.blog-empty {
color: var(--color-text-subtle);
font-style: italic;
font-size: 1rem;
}
.blog-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.blog-card {
padding: 1.5rem 1.75rem;
border-left: 2px solid var(--color-accent-border);
background: var(--color-surface);
border-radius: 0 8px 8px 0;
transition: background 0.2s;
}
.blog-card:hover {
background: var(--color-surface-hover);
}
.blog-card-link {
text-decoration: none;
}
.blog-card-title {
color: var(--color-heading-secondary);
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.3rem;
letter-spacing: -0.01em;
transition: color 0.2s;
}
.blog-card-link:hover .blog-card-title {
color: var(--color-accent);
}
.blog-card-date {
color: var(--color-text-subtle);
font-size: 0.75rem;
display: block;
margin-bottom: 0.5rem;
}
.blog-card-summary {
color: var(--color-text-muted);
margin: 0;
line-height: 1.6;
font-size: 0.875rem;
}
.blog-card-tags {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.blog-tag {
background: var(--color-surface-raised);
color: var(--color-text-muted);
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.7rem;
text-decoration: none;
transition: background 0.2s, color 0.2s;
}
.blog-tag:hover {
background: var(--color-surface-raised-hover);
color: var(--color-heading-secondary);
}
+184
View File
@@ -0,0 +1,184 @@
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-text);
overflow-x: hidden;
font-family: var(--font-body);
line-height: 1.6;
}
.blog-post {
max-width: 800px;
margin: 0 auto;
padding: 4rem clamp(1.5rem, 5vw, 3rem);
}
.blog-back {
color: var(--color-text-subtle);
text-decoration: none;
font-size: 0.85rem;
transition: color 0.2s;
}
.blog-back:hover {
color: var(--color-heading-secondary);
}
.blog-post-header {
margin: 2.5rem 0 2.5rem;
}
.blog-post-title {
color: var(--color-heading);
font-size: clamp(1.75rem, 3vw, 2.25rem);
font-weight: 700;
letter-spacing: -0.02em;
margin: 0 0 0.5rem;
line-height: 1.25;
}
.blog-post-date {
color: var(--color-text-subtle);
font-size: 0.8rem;
display: block;
}
.blog-post-tags {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.blog-tag {
background: var(--color-surface-raised);
color: var(--color-text-muted);
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.7rem;
text-decoration: none;
transition: background 0.2s, color 0.2s;
}
.blog-tag:hover {
background: var(--color-surface-raised-hover);
color: var(--color-heading-secondary);
}
/* ── Post Content ── */
.post-content {
color: var(--color-text);
line-height: 1.8;
font-size: 1rem;
}
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4 {
color: var(--color-heading-secondary);
margin-top: 2rem;
margin-bottom: 0.75rem;
letter-spacing: -0.01em;
}
.post-content h1 { font-size: 1.6rem; }
.post-content h2 { font-size: 1.35rem; }
.post-content h3 { font-size: 1.15rem; }
.post-content p {
margin: 0 0 1.25rem;
}
.post-content a {
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s;
}
.post-content a:hover {
color: var(--color-accent-hover);
}
.post-content ul,
.post-content ol {
margin: 0 0 1.25rem 1.5rem;
}
.post-content li {
margin-bottom: 0.4rem;
}
.post-content code {
background: var(--color-surface-raised);
color: var(--color-heading-secondary);
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.85em;
font-family: var(--font-mono);
}
.post-content pre {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1.25rem 1.5rem;
overflow-x: auto;
margin: 0 0 1.25rem;
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
.post-content pre::-webkit-scrollbar {
height: 6px;
}
.post-content pre::-webkit-scrollbar-track {
background: transparent;
}
.post-content pre::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.post-content pre[class*="language-"] {
background: var(--color-surface);
}
.post-content pre code {
background: none;
color: var(--prism-text);
padding: 0;
font-size: 0.85rem;
}
.post-content blockquote {
border-left: 2px solid var(--color-accent-border);
margin: 0 0 1.25rem;
padding: 0.75rem 1.25rem;
color: var(--color-text-muted);
background: var(--color-surface);
border-radius: 0 8px 8px 0;
}
.post-content img {
max-width: 100%;
border-radius: 8px;
margin: 1.25rem 0;
}
.post-content strong {
color: var(--color-heading-secondary);
}
.post-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}
@@ -0,0 +1,154 @@
/* Prism.js Tomorrow Night theme (customized) */
/* Original: https://github.com/PrismJS/prism/blob/master/themes/prism-tomorrow.css */
/* ============================================
Theme colors — edit these to restyle
============================================ */
:root {
--prism-background: #0a0a0a;
--prism-text: #909090;
--prism-comment: #555;
--prism-punctuation: #707070;
--prism-tag: #09ff00;
--prism-function: #fff;
--prism-boolean: #1ab815;
--prism-number: #1ab815;
--prism-property: #c8c8c8;
--prism-class: #c8c8c8;
--prism-keyword: #09ff00;
--prism-string: #43c760;
--prism-variable: #43c760;
--prism-operator: #999;
--prism-inserted: #09ff00;
}
code[class*="language-"],
pre[class*="language-"] {
color: var(--prism-text);
background: none;
font-family: var(--font-mono);
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
tab-size: 4;
hyphens: none;
}
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: var(--prism-background);
}
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
/* Comments */
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: var(--prism-comment);
}
/* Punctuation */
.token.punctuation {
color: var(--prism-punctuation);
}
/* Tags, attributes, deleted */
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: var(--prism-tag);
}
/* Function names */
.token.function-name,
.token.function {
color: var(--prism-function);
}
/* Booleans */
.token.boolean {
color: var(--prism-boolean);
}
/* Numbers */
.token.number {
color: var(--prism-number);
}
/* Properties */
.token.property {
color: var(--prism-property);
}
/* Classes, constants, symbols */
.token.class-name,
.token.constant,
.token.symbol {
color: var(--prism-class);
}
/* Keywords, selectors, builtins */
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: var(--prism-keyword);
}
/* Strings, chars, attr values, regex */
.token.string,
.token.char,
.token.attr-value,
.token.regex {
color: var(--prism-string);
}
/* Variables */
.token.variable {
color: var(--prism-variable);
}
/* Operators, entities, URLs */
.token.operator,
.token.entity,
.token.url {
color: var(--prism-operator);
}
/* Inserted */
.token.inserted {
color: var(--prism-inserted);
}
/* Formatting */
.token.bold,
.token.important {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
+65
View File
@@ -0,0 +1,65 @@
/* ── Blog Section ── */
#BlogBox {
max-width: 1100px;
margin: clamp(4rem, 8vw, 8rem) auto;
padding: 0 clamp(1.5rem, 5vw, 4rem);
}
#blogPostsList {
display: flex;
flex-direction: column;
gap: 1rem;
}
.blog-teaser-item {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 1.25rem 1.5rem;
border-left: 2px solid var(--color-accent-border);
background: var(--color-surface);
border-radius: 0 8px 8px 0;
transition: background 0.2s;
}
.blog-teaser-item:hover {
background: var(--color-surface-hover);
}
.blog-teaser-title {
font-size: clamp(1rem, 1.5vw, 1.15rem);
font-weight: 600;
color: var(--color-heading-secondary);
text-decoration: none;
transition: color 0.2s;
}
.blog-teaser-title:hover {
color: var(--color-accent);
}
.blog-teaser-date {
font-size: 0.75rem;
color: var(--color-text-subtle);
}
.blog-teaser-summary {
font-size: 0.85rem;
color: var(--color-text-muted);
margin: 0.25rem 0 0;
line-height: 1.6;
}
.blog-view-all {
display: inline-block;
margin-top: 1.5rem;
font-size: 0.85rem;
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s;
}
.blog-view-all:hover {
color: var(--color-accent-hover);
}
+18 -10
View File
@@ -1,17 +1,25 @@
#Contacts { /* ── Footer / Contacts ── */
margin-top: 20vw;
#Contacts {
display: flex;
justify-content: center;
gap: 1.5rem;
padding: clamp(4rem, 8vw, 6rem) 0 clamp(3rem, 5vw, 4rem);
}
.social-link {
display: flex;
align-items: center;
text-decoration: none;
} }
.social-icon { .social-icon {
height: auto; height: auto;
width: 3.5vw; width: clamp(1.25rem, 2.5vw, 1.75rem);
margin-bottom: 5vw; fill: var(--color-text-subtle);
margin-left: 3vw; transition: fill 0.2s;
fill: rgb(212, 212, 212);
transition: fill 0.5s;
} }
.social-icon:hover { .social-link:hover .social-icon {
fill: white; fill: var(--color-heading-secondary);
transition: fill 0.5s;
} }
@@ -1,36 +0,0 @@
.diagonal-section-left {
position: absolute;
clip-path: polygon(0 0, 85vw 0, 60vw 100%, 0 100%);
background-color: rgb(104, 255, 0, 0.39);
transition: clip-path 0.5s ease;
}
.diagonal-section-left:has(.projectImage:hover) {
clip-path: polygon(0 0, 87vw 0, 58vw 100%, 0 100%);
transition: clip-path 0.5s ease;
}
.projectImageLeft {
width: 33vw;
height: auto;
margin: 3vw;
}
.projectTextRight {
position: relative;
z-index: 1;
margin-left: 50vw;
}
.projectHeaderRight {
margin: 0;
color: rgb(212, 212, 212);
font-size: 4vw;
}
.projectDescriptionRight {
margin: 0;
color: rgb(212, 212, 212);
font-size: 2vw;
width: 40vw;
}
@@ -1,38 +0,0 @@
.diagonal-section-right {
position: absolute;
clip-path: polygon(40vw 0, 100% 0, 100% 100%, 15vw 100%);
background-color: rgb(104, 255, 0, 0.39);
transition: clip-path 0.5s ease;
}
.diagonal-section-right:has(.projectImage:hover) {
clip-path: polygon(42vw 0, 100% 0, 100% 100%, 13vw 100%);
transition: clip-path 0.5s ease;
}
.projectImageRight {
width: 33vw;
height: auto;
margin: 3vw;
float: right;
}
.projectTextLeft {
position: relative;
z-index: 1;
}
.projectHeaderLeft {
margin: 0;
color: rgb(212, 212, 212);
font-size: 4vw;
margin-left: 10vw;
}
.projectDescriptionLeft {
margin: 0;
color: rgb(212, 212, 212);
font-size: 2vw;
width: 40vw;
margin-left: 10vw;
}
+77 -16
View File
@@ -1,29 +1,90 @@
#ProjectsBox { /* ── Projects Section ── */
#ProjectsBox {
max-width: 1100px;
margin: 0 auto;
padding: 0 clamp(1.5rem, 5vw, 4rem);
}
.projects-grid {
display: grid; display: grid;
grid-auto-rows: 1fr; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
gap: 20vw; gap: 1.5rem;
} }
.projectContainer { /* ── Card ── */
position: relative;
display: flex; .project-card {
align-items: center; border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
background: var(--color-surface);
transition: border-color 0.25s, transform 0.25s;
} }
.displayBox { .project-card:hover {
width: 100vw; border-color: var(--color-border-hover);
height: auto; transform: translateY(-2px);
} }
a.projectContainer { /* ── Image ── */
text-decoration: none;
.project-image-link {
display: block;
overflow: hidden;
} }
.projectImage { .projectImage {
transition: width 0.5s ease; width: 100%;
height: auto;
display: block;
transition: transform 0.4s ease;
} }
.projectImage:hover { .project-card:hover .projectImage {
width: 40vw; transform: scale(1.03);
transition: width 0.5s ease; }
/* ── Info ── */
.project-info {
padding: 1.25rem 1.5rem 1.5rem;
}
.project-title {
margin: 0 0 0.5rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--color-heading);
letter-spacing: -0.01em;
}
.project-description {
margin: 0 0 1rem;
font-size: 0.85rem;
line-height: 1.65;
color: var(--color-text-muted);
}
.project-link {
font-size: 0.8rem;
color: var(--color-accent);
text-decoration: none;
transition: color 0.2s;
}
.project-link:hover {
color: var(--color-accent-hover);
}
/* ── Mobile ── */
@media (max-width: 600px) {
.projects-grid {
grid-template-columns: 1fr;
}
.project-info {
padding: 1rem 1.25rem 1.25rem;
}
} }
+108 -41
View File
@@ -1,67 +1,134 @@
body { *, *::before, *::after {
margin: 0px; box-sizing: border-box;
background-color: #2b2c30;
color: #09ff00;
overflow-x: hidden;
cursor: default;
} }
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-text);
overflow-x: hidden;
font-family: var(--font-body);
line-height: 1.6;
}
/* ── Navigation ── */
#Header { #Header {
margin-top: 2vw; display: flex;
justify-content: space-between;
align-items: center;
max-width: 1100px;
margin: 0 auto;
padding: 2rem clamp(1.5rem, 5vw, 4rem);
} }
.headerOption { .site-name {
font: bold 2vw arial; font-size: 1.05rem;
display: inline; font-weight: 700;
margin-left: 10vw; color: var(--color-heading);
letter-spacing: -0.01em;
}
.nav-links {
display: flex;
gap: clamp(1.25rem, 3vw, 2.5rem);
}
.nav-link {
font-size: 0.875rem;
color: var(--color-text-subtle);
cursor: pointer; cursor: pointer;
transition: color 0.5s ease, font 0.5s ease; transition: color 0.2s;
text-decoration: none;
} }
.headerOption:hover { .nav-link:hover {
font: bold 2.3vw arial; color: var(--color-heading-secondary);
transition: color 0.5s ease, font 0.5s ease; }
/* ── Hero / Welcome Message ── */
#Hero {
max-width: 1100px;
margin: 0 auto;
padding: clamp(4rem, 10vw, 8rem) clamp(1.5rem, 5vw, 4rem) clamp(4rem, 8vw, 6rem);
} }
#WelcomeMessage { #WelcomeMessage {
font: 3vw arial; font-size: clamp(1.25rem, 2.2vw, 1.75rem);
width: auto; max-width: 720px;
height: 40vw; min-height: clamp(12rem, 28vw, 20rem);
margin-top: 15vw; line-height: 1.7;
text-align: left; color: var(--color-accent);
padding-left: 10vw; font-family: var(--font-mono);
padding-right: 10vw;
} }
/* ── Section Titles ── */
.section-title {
font-size: clamp(1.3rem, 2.2vw, 1.75rem);
font-weight: 700;
color: var(--color-heading);
margin: 0 0 clamp(1.5rem, 3vw, 2.5rem);
letter-spacing: -0.02em;
}
/* ── Demos ── */
#DemosBox { #DemosBox {
margin: 10vw; max-width: 1100px;
display: flex; margin: clamp(4rem, 8vw, 8rem) auto;
flex-direction: column; padding: 0 clamp(1.5rem, 5vw, 4rem);
justify-content: end;
width: 80%;
} }
#DemoHeader { #buttonWrapper {
margin: 0; display: grid;
color: rgb(212, 212, 212); grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
font-size: 4vw; gap: 0.75rem;
} }
.demoButton { .demoButton {
margin-top: 5vw; padding: 0.875rem 1.25rem;
margin-right: 2vw;
padding-top: 0.5vw;
padding-bottom: 0.5vw;
text-align: center; text-align: center;
border: none; border: 1px solid var(--color-border);
border-radius: 5vw; border-radius: 8px;
width: 20vw; font-size: 0.875rem;
height: 8vw; font-family: inherit;
font-size: 2vw; background: var(--color-surface);
background-color: rgb(104, 255, 0, 0.39); color: var(--color-text);
cursor: pointer; cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.demoButton:hover {
background: var(--color-surface-hover);
border-color: var(--color-border-hover);
color: var(--color-heading-secondary);
} }
#MoreFiller { #MoreFiller {
cursor: default; cursor: default;
opacity: 0.3;
}
#MoreFiller:hover {
background: var(--color-surface);
border-color: var(--color-border);
color: var(--color-text);
}
/* ── Mobile ── */
@media (max-width: 600px) {
#Header {
padding: 1.25rem 1.25rem;
}
.site-name {
font-size: 0.95rem;
}
.nav-link {
font-size: 0.8rem;
}
} }
+28
View File
@@ -0,0 +1,28 @@
:root {
/* Backgrounds */
--color-bg: #0a0a0a;
--color-surface: rgba(255, 255, 255, 0.02);
--color-surface-hover: rgba(255, 255, 255, 0.04);
--color-surface-raised: rgba(255, 255, 255, 0.05);
--color-surface-raised-hover: rgba(255, 255, 255, 0.08);
/* Text */
--color-heading: #fff;
--color-heading-secondary: #e0e0e0;
--color-text: #b0b0b0;
--color-text-muted: #aaa;
--color-text-subtle: #777;
/* Accent */
--color-accent: #09ff00;
--color-accent-hover: #5fff5f;
/* Borders */
--color-border: rgba(255, 255, 255, 0.07);
--color-border-hover: rgba(255, 255, 255, 0.15);
--color-accent-border: rgba(9, 255, 0, 0.4);
/* Typography */
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
}
+9 -2
View File
@@ -45,9 +45,9 @@ function simulateTyping(element, text, speed = 50) {
function showClickedContents(option) { function showClickedContents(option) {
if (option === 'projects') { if (option === 'projects') {
document.querySelector("#ChessProject > div.diagonal-section-left").scrollIntoView({ document.querySelector("#ProjectsBox").scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "center", block: "start",
inline: "nearest" inline: "nearest"
}); });
} }
@@ -68,4 +68,11 @@ function showClickedContents(option) {
inline: "nearest" inline: "nearest"
}); });
} }
else if (option === 'blog') {
document.querySelector("#BlogBox").scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest"
});
}
} }