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]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5fd3a5d7c1
commit
8043eee4cc
@@ -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;
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@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">← 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" />
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
<div id="WelcomeMessage"></div>
|
<a class="nav-link" onclick="showClickedContents('contact')">Contact</a>
|
||||||
|
|
||||||
<div id="ProjectsBox">
|
|
||||||
<div id="ChessProject" class="projectContainer">
|
|
||||||
<div class="displayBox diagonal-section-left">
|
|
||||||
<a target="_blank" href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml">
|
|
||||||
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="ChessText" class="projectTextRight">
|
|
||||||
<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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div id="CompilerProject" class="projectContainer">
|
<section id="Hero">
|
||||||
<div id="CompilerText" class="projectTextLeft">
|
<div id="WelcomeMessage"></div>
|
||||||
<h2 id="CompilerTitle" class="projectHeaderLeft projectHeader">Compiler</h2>
|
</section>
|
||||||
<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.
|
<section id="ProjectsBox">
|
||||||
</p>
|
<h2 class="section-title">Projects</h2>
|
||||||
</div>
|
<div class="projects-grid">
|
||||||
<div class="displayBox diagonal-section-right">
|
<article class="project-card">
|
||||||
<a target="_blank" href="https://github.com/JoshHeaps/CILCompiler">
|
<a href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml" target="_blank" class="project-image-link">
|
||||||
<img id="CompilerImage" class="projectImage projectImageRight" src="/images/CompilerDemo.png" title="Compiler Input and Output"/>
|
<img class="projectImage" src="/images/Chess.jpg" alt="Chessboard" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div class="project-info">
|
||||||
</div>
|
<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 →</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<div id="CloudStorageProject" class="projectContainer">
|
<article class="project-card">
|
||||||
<div class="displayBox diagonal-section-left">
|
<a href="https://github.com/JoshHeaps/CILCompiler" target="_blank" class="project-image-link">
|
||||||
<a target="_blank" href="https://media.joshheaps.net">
|
<img class="projectImage" src="/images/CompilerDemo.png" alt="Compiler Input and Output" />
|
||||||
<img id="CloudStorageImage" class="projectImage projectImageLeft" src="/images/CloudStorageDemo.png" title="CloudStorageScreenshot" />
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div class="project-info">
|
||||||
<div id="CloudStorageText" class="projectTextRight">
|
<h3 class="project-title">Compiler</h3>
|
||||||
<h2 id="CloudStorageTitle" class="projectHeaderRight projectHeader">Image Cloud Storage</h2>
|
<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>
|
||||||
<p id="CloudStorageDescription" class="projectDescriptionRight projectDescription">
|
<a class="project-link" href="https://github.com/JoshHeaps/CILCompiler" target="_blank">View on GitHub →</a>
|
||||||
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.
|
</div>
|
||||||
</p>
|
</article>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="DemosBox" class="displayBox">
|
<article class="project-card">
|
||||||
<h2 id="DemoHeader">Demos</h2>
|
<a href="https://media.joshheaps.net" target="_blank" class="project-image-link">
|
||||||
|
<img class="projectImage" src="/images/CloudStorageDemo.png" alt="Cloud Storage Screenshot" />
|
||||||
|
</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 →</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="DemosBox">
|
||||||
|
<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 →</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer id="Contacts">
|
<footer id="Contacts">
|
||||||
<svg viewBox="0 0 16 16" class="social-icon">
|
<a class="social-link" href="https://github.com/JoshHeaps" target="_blank">
|
||||||
<a id="GithubLink" class="social-link" href="https://github.com/JoshHeaps" target="_blank">
|
<svg viewBox="0 0 16 16" class="social-icon">
|
||||||
<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" />
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</svg>
|
<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" />
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</svg>
|
<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" />
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</svg>
|
|
||||||
</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" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,4 +117,4 @@
|
|||||||
<script src="/js/site.js"></script>
|
<script src="/js/site.js"></script>
|
||||||
<script src="/js/utils.js"></script>
|
<script src="/js/utils.js"></script>
|
||||||
<script src="/js/opacityUpdateObserver.js"></script>
|
<script src="/js/opacityUpdateObserver.js"></script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,88 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CacheEntry(object Value, DateTime ExpiresAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -5,5 +5,8 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"BlogApi": {
|
||||||
|
"BaseUrl": "https://media.joshheaps.net"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
*, *::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content pre code {
|
||||||
|
background: none;
|
||||||
|
color: var(--color-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%;
|
||||||
|
height: auto;
|
||||||
|
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,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);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user