Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d468a46e | ||
|
|
ea60ee5a4b | ||
|
|
99a9474ef7 | ||
|
|
8043eee4cc | ||
|
|
5fd3a5d7c1 | ||
|
|
9969c6e3cf | ||
|
|
0788015cb0 | ||
|
|
34de3c1abc | ||
|
|
311d62a719 | ||
|
|
4217bf3703 | ||
|
|
fd68ada82e | ||
|
|
7c77cd0e52 | ||
|
|
93bdb3518a | ||
|
|
69e1e8450d | ||
|
|
e74de40d2e | ||
|
|
9b86bfafe0 |
@@ -3,7 +3,9 @@
|
||||
"allow": [
|
||||
"Bash(dotnet test:*)",
|
||||
"Bash(dotnet build)",
|
||||
"Bash(dir:*)"
|
||||
"Bash(dir:*)",
|
||||
"Bash(dotnet add:*)",
|
||||
"Bash(dotnet build:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -11,6 +11,19 @@ public class ApiTests : PageTest
|
||||
private IAPIRequestContext? _apiContext;
|
||||
private TestConfiguration Config => TestConfiguration.Instance;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task OneTimeSetUp()
|
||||
{
|
||||
if (!Config.Playwright.Headless)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||
}
|
||||
if (Config.Playwright.SlowMotion > 0)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class ChessController(
|
||||
private static readonly ConcurrentDictionary<Guid, CancellationTokenSource> _gameRemovalCancellationTokens = [];
|
||||
|
||||
private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1);
|
||||
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromDays(1);
|
||||
private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
@@ -165,7 +165,7 @@ public class ChessController(
|
||||
var expectedPlayerId = isWhiteMove ? gameState.WhitePlayerId : gameState.BlackPlayerId;
|
||||
|
||||
if (moveDto.PlayerId != expectedPlayerId)
|
||||
return Forbid("You are not the current player.");
|
||||
return StatusCode(403, "You are not the current player.");
|
||||
|
||||
// Make sure player owns the piece
|
||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
|
||||
@@ -174,7 +174,7 @@ public class ChessController(
|
||||
return NotFound("Chess piece Id does not exist");
|
||||
|
||||
if ((isWhiteMove && piece.Color != PieceColor.White) || (!isWhiteMove && piece?.Color != PieceColor.Black))
|
||||
return Forbid("You cannot move this piece.");
|
||||
return StatusCode(403, "You cannot move this piece.");
|
||||
|
||||
var result = chessService.MakeMove(gameState, moveDto);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>53ed685c-bdff-4306-8cc2-9fbe55c85713</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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,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">← 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>
|
||||
}
|
||||
@@ -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,101 +1,115 @@
|
||||
@page
|
||||
@page
|
||||
@model JoshHeaps.Net.Pages.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "JoshHeaps.Net";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<header id="Header">
|
||||
<a class="headerOption" onclick="showClickedContents('projects')">./ Projects</a>
|
||||
<a class="headerOption" onclick="showClickedContents('demos')">./ Demos</a>
|
||||
<a class="headerOption" onclick="showClickedContents('contact')">./ Contact me</a>
|
||||
</header>
|
||||
<nav id="Header">
|
||||
<span class="site-name">josh heaps</span>
|
||||
<div class="nav-links">
|
||||
<a class="nav-link" onclick="showClickedContents('projects')">Projects</a>
|
||||
<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">
|
||||
<div id="ChessProject" class="projectContainer">
|
||||
<div class="displayBox diagonal-section-left">
|
||||
<a target="_blank" href="https://github.com/JoshHeaps/OnlineChess">
|
||||
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/>
|
||||
<section id="ProjectsBox">
|
||||
<h2 class="section-title">Projects</h2>
|
||||
<div class="projects-grid">
|
||||
<article class="project-card">
|
||||
<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>
|
||||
<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 →</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. This project is a work in progress, as currently it can only be played locally. It was built in C# on .NET 8.0, using winforms, because I like a challenge. I plan to add online multiplayer functionality in the future.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div id="CompilerProject" class="projectContainer">
|
||||
<div id="CompilerText" class="projectTextLeft">
|
||||
<h2 id="CompilerTitle" class="projectHeaderLeft projectHeader">Compiler</h2>
|
||||
<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"/>
|
||||
<article class="project-card">
|
||||
<a href="https://github.com/JoshHeaps/CILCompiler" target="_blank" class="project-image-link">
|
||||
<img class="projectImage" src="/images/CompilerDemo.png" alt="Compiler Input and Output" />
|
||||
</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 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div id="DemosBox" class="displayBox">
|
||||
<h2 id="DemoHeader">Demos</h2>
|
||||
<article class="project-card">
|
||||
<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">
|
||||
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/chess'">
|
||||
Play Chess
|
||||
</button>
|
||||
<button id="ParticleDemo" class="demoButton" onclick="window.location.href='/particles'">
|
||||
Particle Simulator
|
||||
</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>
|
||||
<button class="demoButton" onclick="window.location.href='/chess'">Play Chess</button>
|
||||
<button class="demoButton" onclick="window.location.href='/particles'">Particle Simulator</button>
|
||||
<button class="demoButton" onclick="window.location.href='/memorylane'">Memory Lane</button>
|
||||
<button 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>
|
||||
</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">
|
||||
<a class="social-link" href="https://github.com/JoshHeaps" target="_blank">
|
||||
<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>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a class="social-link" href="mailto:[email protected]">
|
||||
<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="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="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"/>
|
||||
<rect width="100%" height="100%" fill="transparent" />
|
||||
</a>
|
||||
</svg>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
@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/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/imageRightProject.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/footer.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JoshHeaps.Net.Pages
|
||||
{
|
||||
public class IndexModel() : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
public class IndexModel(IBlogService blogService) : PageModel
|
||||
{
|
||||
public List<BlogPost> LatestPosts { get; set; } = [];
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var allPosts = await blogService.GetAllPostsAsync();
|
||||
LatestPosts = allPosts.Take(3).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@{
|
||||
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>
|
||||
|
||||
@@ -12,6 +12,13 @@ builder.Services.AddControllers();
|
||||
|
||||
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<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
|
||||
|
||||
@@ -8,31 +8,23 @@ public class AutoIpUpdateService(
|
||||
ILogger<AutoIpUpdateService> log)
|
||||
: BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
|
||||
private static readonly HttpClient httpClient = new();
|
||||
public static bool IsEnabled { get; private set; } = false;
|
||||
|
||||
private static readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stop)
|
||||
{
|
||||
IsEnabled = true;
|
||||
var timer = new PeriodicTimer(CheckInterval);
|
||||
AAAARecord dnsRecord = await GetDnsRecordAsync();
|
||||
string lastKnownIp = dnsRecord.Content;
|
||||
var timer = new PeriodicTimer(_checkInterval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stop))
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentIp = await GetPublicIpAsync() ?? "";
|
||||
|
||||
if (lastKnownIp != currentIp)
|
||||
{
|
||||
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
|
||||
|
||||
lastKnownIp = currentIp;
|
||||
await UpdateIpAddressIfChanged();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutting down */ }
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError(ex, "Error while attempting ip update");
|
||||
@@ -40,11 +32,33 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateIpAddressIfChanged()
|
||||
{
|
||||
var dnsRecords = await GetDnsRecordAsync();
|
||||
string dnsRecordIp = dnsRecords[0].Content;
|
||||
|
||||
if (!dnsRecords.Records.All(x => x.Content == dnsRecords[0].Content))
|
||||
dnsRecordIp = string.Empty;
|
||||
|
||||
string publicIp = await GetPublicIpAsync() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(publicIp) || dnsRecordIp == publicIp)
|
||||
return;
|
||||
|
||||
foreach (var dnsRecord in dnsRecords.Records)
|
||||
{
|
||||
if (dnsRecord.Content == publicIp)
|
||||
continue;
|
||||
|
||||
await UpdateDnsIpAsync(config, dnsRecord, publicIp);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> GetPublicIpAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||
return await _httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -55,7 +69,7 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AAAARecord> GetDnsRecordAsync()
|
||||
private async Task<RecordList> GetDnsRecordAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -64,8 +78,8 @@ public class AutoIpUpdateService(
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
||||
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
||||
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!.Result[0];
|
||||
var records = JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -76,22 +90,23 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateDnsIpAsync(IConfiguration config, AAAARecord record, string ip)
|
||||
private static async Task UpdateDnsIpAsync(IConfiguration config, DnsRecord record, string ip)
|
||||
{
|
||||
HttpClient cfClient = new();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
object content = new
|
||||
|
||||
object updateRequestBody = new
|
||||
{
|
||||
comment = "Update as needed",
|
||||
name = record.Name,
|
||||
ttl = record.Ttl,
|
||||
type = record.Type,
|
||||
comment = record.Comment,
|
||||
content = ip,
|
||||
name = "@",
|
||||
proxied = true,
|
||||
ttl = 3600,
|
||||
type = "AAAA"
|
||||
proxied = record.Proxied,
|
||||
};
|
||||
|
||||
var result = await cfClient.PutAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records{record.Id}", content);
|
||||
var result = await cfClient.PatchAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records/{record.Id}", updateRequestBody);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -102,11 +117,23 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
record AAAARecord(
|
||||
record DnsRecord(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("ttl")] int Ttl,
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("comment")] string Comment,
|
||||
[property: JsonPropertyName("content")] string Content,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("proxied")] bool Proxied,
|
||||
[property: JsonPropertyName("id")] string Id);
|
||||
|
||||
record RecordList([property: JsonPropertyName("result")] List<AAAARecord> Result);
|
||||
record RecordList([property: JsonPropertyName("result")] List<DnsRecord> Records)
|
||||
{
|
||||
public DnsRecord this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return Records[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -110,7 +110,6 @@ public class ChessService : IChessService
|
||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == pieceId);
|
||||
|
||||
if (piece == null) return [];
|
||||
if (piece.Color != gameState.CurrentPlayer) return [];
|
||||
|
||||
var candidateMoves = GenerateCandidateMoves(gameState, piece);
|
||||
var legalMoves = new List<Position>();
|
||||
@@ -529,8 +528,15 @@ public class ChessService : IChessService
|
||||
var ep = gs.EnPassantTarget.Value;
|
||||
|
||||
if (ep.Row == forward1 && Math.Abs(ep.Col - startCol) == 1)
|
||||
{
|
||||
// Verify there's an enemy pawn to capture
|
||||
int enemyPawnRow = piece.Color == PieceColor.White ? ep.Row + 1 : ep.Row - 1;
|
||||
var enemyPawn = gs.Board[enemyPawnRow, ep.Col];
|
||||
|
||||
if (enemyPawn != null && enemyPawn.Type == PieceType.Pawn && enemyPawn.Color != piece.Color)
|
||||
moves.Add(ep);
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
@@ -542,11 +548,7 @@ public class ChessService : IChessService
|
||||
=> GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]);
|
||||
|
||||
private static List<Position> GenerateQueenMoves(GameState gs, ChessPiece piece)
|
||||
=> GenerateSlidingMoves(gs, piece,
|
||||
[
|
||||
(1, 0), (-1, 0), (0, 1), (0, -1),
|
||||
(1, 1), (1, -1), (-1, 1), (-1, -1)
|
||||
]);
|
||||
=> [..GenerateRookMoves(gs, piece), ..GenerateBishopMoves(gs, piece)];
|
||||
|
||||
private static List<Position> GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions)
|
||||
{
|
||||
|
||||
@@ -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,5 +5,9 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"BlogApi": {
|
||||
"BaseUrl": "https://media.joshheaps.net",
|
||||
"InvalidateKey": "CHANGE_ME"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
#chessBoard {
|
||||
#boardContainer {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: 2vw auto;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 60vw;
|
||||
height: 60vw;
|
||||
display: grid;
|
||||
@@ -159,3 +164,29 @@
|
||||
height: 50px;
|
||||
pointer-events: none; /* ensures img doesn't steal the click */
|
||||
}
|
||||
|
||||
.chessSquare .coordinate-label {
|
||||
position: absolute;
|
||||
font-size: 1.2vw;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chessSquare.light .coordinate-label {
|
||||
color: #656770;
|
||||
}
|
||||
|
||||
.chessSquare.dark .coordinate-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.chessSquare .row-label {
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.chessSquare .col-label {
|
||||
bottom: 2px;
|
||||
right: 4px;
|
||||
}
|
||||
@@ -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 {
|
||||
margin-top: 20vw;
|
||||
/* ── Footer / Contacts ── */
|
||||
|
||||
#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 {
|
||||
height: auto;
|
||||
width: 3.5vw;
|
||||
margin-bottom: 5vw;
|
||||
margin-left: 3vw;
|
||||
fill: rgb(212, 212, 212);
|
||||
transition: fill 0.5s;
|
||||
width: clamp(1.25rem, 2.5vw, 1.75rem);
|
||||
fill: var(--color-text-subtle);
|
||||
transition: fill 0.2s;
|
||||
}
|
||||
|
||||
.social-icon:hover {
|
||||
fill: white;
|
||||
transition: fill 0.5s;
|
||||
.social-link:hover .social-icon {
|
||||
fill: var(--color-heading-secondary);
|
||||
}
|
||||
@@ -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;
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 20vw;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.projectContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* ── Card ── */
|
||||
|
||||
.project-card {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--color-surface);
|
||||
transition: border-color 0.25s, transform 0.25s;
|
||||
}
|
||||
|
||||
.displayBox {
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
.project-card:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
a.projectContainer {
|
||||
text-decoration: none;
|
||||
/* ── Image ── */
|
||||
|
||||
.project-image-link {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projectImage {
|
||||
transition: width 0.5s ease;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.projectImage:hover {
|
||||
width: 40vw;
|
||||
transition: width 0.5s ease;
|
||||
.project-card:hover .projectImage {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
margin: 0px;
|
||||
background-color: #2b2c30;
|
||||
color: #09ff00;
|
||||
overflow-x: hidden;
|
||||
cursor: default;
|
||||
*, *::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;
|
||||
}
|
||||
|
||||
/* ── Navigation ── */
|
||||
|
||||
#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 {
|
||||
font: bold 2vw arial;
|
||||
display: inline;
|
||||
margin-left: 10vw;
|
||||
.site-name {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
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;
|
||||
transition: color 0.5s ease, font 0.5s ease;
|
||||
transition: color 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.headerOption:hover {
|
||||
font: bold 2.3vw arial;
|
||||
transition: color 0.5s ease, font 0.5s ease;
|
||||
.nav-link:hover {
|
||||
color: var(--color-heading-secondary);
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
font: 3vw arial;
|
||||
width: auto;
|
||||
height: 40vw;
|
||||
margin-top: 15vw;
|
||||
text-align: left;
|
||||
padding-left: 10vw;
|
||||
padding-right: 10vw;
|
||||
font-size: clamp(1.25rem, 2.2vw, 1.75rem);
|
||||
max-width: 720px;
|
||||
min-height: clamp(12rem, 28vw, 20rem);
|
||||
line-height: 1.7;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
margin: 10vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
width: 80%;
|
||||
max-width: 1100px;
|
||||
margin: clamp(4rem, 8vw, 8rem) auto;
|
||||
padding: 0 clamp(1.5rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
#DemoHeader {
|
||||
margin: 0;
|
||||
color: rgb(212, 212, 212);
|
||||
font-size: 4vw;
|
||||
#buttonWrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.demoButton {
|
||||
margin-top: 5vw;
|
||||
margin-right: 2vw;
|
||||
padding-top: 0.5vw;
|
||||
padding-bottom: 0.5vw;
|
||||
padding: 0.875rem 1.25rem;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-radius: 5vw;
|
||||
width: 20vw;
|
||||
height: 8vw;
|
||||
font-size: 2vw;
|
||||
background-color: rgb(104, 255, 0, 0.39);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 132 KiB |
@@ -27,9 +27,14 @@ const ChessAPI = {
|
||||
body: JSON.stringify(moveDto)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = await response.text();
|
||||
throw new Error(message || "Invalid move or not your turn.");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Invalid move or not your turn.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const ChessBoard = {
|
||||
renderPieces(pieces) {
|
||||
this.clearAllSquares();
|
||||
this.renderCoordinateLabels();
|
||||
this.placePieces(pieces);
|
||||
this.setupSquareEventHandlers();
|
||||
this.highlightPreviousMove();
|
||||
@@ -106,6 +107,39 @@ const ChessBoard = {
|
||||
ChessAPI.handleMove(targetRow, targetCol);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCoordinateLabels() {
|
||||
const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
||||
const ranks = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
||||
|
||||
// If player is black, reverse the coordinates
|
||||
if (!GameState.currentPlayerIsWhite) {
|
||||
files.reverse();
|
||||
ranks.reverse();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const square = document.getElementById(`square-${i}`);
|
||||
const row = Math.floor(i / 8);
|
||||
const col = i % 8;
|
||||
|
||||
// Add rank label (1-8) on the leftmost column
|
||||
if (col === 0) {
|
||||
const rankLabel = document.createElement('span');
|
||||
rankLabel.className = 'coordinate-label row-label';
|
||||
rankLabel.textContent = ranks[row];
|
||||
square.appendChild(rankLabel);
|
||||
}
|
||||
|
||||
// Add file label (a-h) on the bottom row
|
||||
if (row === 7) {
|
||||
const fileLabel = document.createElement('span');
|
||||
fileLabel.className = 'coordinate-label col-label';
|
||||
fileLabel.textContent = files[col];
|
||||
square.appendChild(fileLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
element.style.display = "none"; // Trigger reflow
|
||||
element.offsetHeight; // Force reflow
|
||||
element.style.display = ""; // Restore the original display
|
||||
typeText("#WelcomeMessage", "Hey, you found me...!?``````````````````````No that's stupid````````````````Just````uhhhh`````Hello! My name is Josh. I'm a software engineer, and I try to have a lot of fun with what I do. While I have your attention, why don't I tell you more about myself?");
|
||||
typeText("#WelcomeMessage", "Hey, you found me...!?``````````````````````No that's stupid...```````````````````Just...```````uhhhh...````````Hello! My name is Josh. I'm a software engineer, and I try to have a lot of fun with what I do. While I have your attention, why don't I tell you more about myself?");
|
||||
}
|
||||
@@ -12,18 +12,19 @@
|
||||
let intervalId;
|
||||
let index = 0;
|
||||
const punctuation = ['.', '!', '?'];
|
||||
const punctuationDict = {
|
||||
'.': 150,
|
||||
'!': 200,
|
||||
'?': 200,
|
||||
',': 100,
|
||||
'`': 25,
|
||||
};
|
||||
|
||||
let currentText = "";
|
||||
|
||||
function changeIntervalTime(element, text) {
|
||||
if (punctuation.includes(text.charAt(index))) {
|
||||
clearInterval(intervalId);
|
||||
simulateTyping(element, text, 200);
|
||||
}
|
||||
else {
|
||||
clearInterval(intervalId);
|
||||
simulateTyping(element, text);
|
||||
}
|
||||
simulateTyping(element, text, punctuationDict[text.charAt(index)] ?? 50);
|
||||
}
|
||||
|
||||
function simulateTyping(element, text, speed = 50) {
|
||||
@@ -44,9 +45,9 @@ function simulateTyping(element, text, speed = 50) {
|
||||
|
||||
function showClickedContents(option) {
|
||||
if (option === 'projects') {
|
||||
document.querySelector("#ChessProject > div.diagonal-section-left").scrollIntoView({
|
||||
document.querySelector("#ProjectsBox").scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
block: "start",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
@@ -67,4 +68,11 @@ function showClickedContents(option) {
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
else if (option === 'blog') {
|
||||
document.querySelector("#BlogBox").scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user