Author SHA1 Message Date
jheaps 34de3c1abc Update href for chess github 2025-10-17 12:57:17 -06:00
jheaps 311d62a719 Update images 2025-10-17 12:48:10 -06:00
jheaps 4217bf3703 Fix headless issue 2025-10-17 12:29:49 -06:00
jheaps fd68ada82e Get fancy wit it 2025-10-17 12:14:37 -06:00
jheaps 7c77cd0e52 Add square numbers and bug fixes 2025-10-17 12:10:08 -06:00
jheaps 93bdb3518a Bug fixes 2025-10-17 11:41:50 -06:00
Josh Heaps 69e1e8450d Merge pull request #10 from JoshHeaps/tech-debt/chess-cleanup
Add demo button for media.joshheaps.net
2025-10-13 10:18:17 -06:00
jheaps 09b408a75b Add demo button for media.joshheaps.net 2025-10-13 10:15:44 -06:00
Josh Heaps e74de40d2e Merge pull request #9 from JoshHeaps/tech-debt/chess-cleanup
Tech debt/chess cleanup
2025-10-03 11:27:23 -06:00
jheaps 03dd308449 game removal code cleanup 2025-10-03 11:24:02 -06:00
jheaps 54e4f25be7 Styling 2025-10-03 10:45:20 -06:00
jheaps c2622132bc Revert site back to normal, add memory lane 2025-10-03 10:43:15 -06:00
jheaps 9bd7eb283e Remove bad test 2025-10-03 10:42:57 -06:00
Josh Heaps 9b86bfafe0 Merge pull request #8 from JoshHeaps/tech-debt/chess-cleanup
Tech debt/chess cleanup
2025-10-02 10:19:41 -06:00
jheaps 95b4ae63ea Anniversary page <3 2025-10-02 10:16:05 -06:00
jheaps edab13f8e4 Javascript cleanup 2025-09-25 10:46:24 -06:00
Josh Heaps ea78302041 Merge pull request #7 from JoshHeaps/tests/CreatePlaywrightTests
Add playwright tests with github actions
2025-09-24 14:43:01 -06:00
Josh Heaps 3b91055666 Merge pull request #6 from JoshHeaps/UpdateParticleSimulatorButton
Update particle simulator button
2025-09-19 16:31:17 -06:00
jheaps b4acdee6e3 Fix auto ip updates 2025-09-19 16:29:44 -06:00
jheaps 10bf9b3d30 Bring debug.js back (I don't remember killing it) 2025-09-19 16:09:47 -06:00
Josh Heaps a3b384b221 Merge pull request #5 from JoshHeaps/UpdateParticleSimulatorButton
remove button to show off github actions
2025-09-02 11:20:47 -06:00
jheaps be0a753945 remove button to show off github actions 2025-09-02 11:18:44 -06:00
Josh Heaps eb3d15527a Merge pull request #4 from JoshHeaps/UpdateParticleSimulatorButton
aewpoifhjp
2025-09-02 11:15:27 -06:00
jheaps bb0d611b21 aewpoifhjp 2025-09-02 11:14:49 -06:00
Josh Heaps 636f133288 Merge pull request #3 from JoshHeaps/UpdateParticleSimulatorButton
Update particle simulator button
2025-08-29 16:31:21 -06:00
jheaps 847f5cc970 Update button sizing 2025-08-29 16:30:18 -06:00
jheaps 75d4ee8f9a Update button text 2025-08-29 16:28:35 -06:00
jheaps d0272adb51 stop running build twice after push 2025-08-29 16:27:37 -06:00
34 changed files with 1331 additions and 110 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
{
"permissions": {
"allow": [
"Bash(dotnet test:*)"
"Bash(dotnet test:*)",
"Bash(dotnet build)",
"Bash(dir:*)"
],
"deny": [],
"ask": []
-2
View File
@@ -4,8 +4,6 @@
name: .NET
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
+13
View File
@@ -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()
{
-14
View File
@@ -81,18 +81,4 @@ public class MultiplayerTests : PageTest
// Should see SignalR connected message in console
Assert.That(consoleLogs, Does.Contain("✅ SignalR connected").Or.Contain("SignalR connected"));
}
[Test]
public async Task Move_Updates_Are_Sent_Via_SignalR()
{
// This would require more complex setup with two players
// For now, just verify the SignalR methods exist in the JavaScript
var jsContent = await Page.EvaluateAsync<string>(@"
() => {
return document.querySelector('script[src*=""chessLogic.js""]') ? 'chessLogic.js loaded' : 'not found';
}
");
Assert.That(jsContent, Is.EqualTo("chessLogic.js loaded"));
}
}
+41 -36
View File
@@ -9,14 +9,21 @@ namespace JoshHeaps.Net.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ChessController(IChessService chessService, IHubContext<ChessHub> chessHub, IBackgroundTaskQueue queue) : ControllerBase
public class ChessController(
IChessService chessService,
IHubContext<ChessHub> chessHub,
IBackgroundTaskQueue queue) : ControllerBase
{
/// <summary>
/// Store of ongoing games.
/// </summary>
private static readonly ConcurrentDictionary<Guid, GameState> _games = [];
private static readonly ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
private static readonly ConcurrentDictionary<Guid, CancellationTokenSource> _gameRemovalCancellationTokens = [];
private static ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1);
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromDays(1);
private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1);
/// <summary>
/// Create a new chess game and store it in-memory.
@@ -54,7 +61,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
});
}
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
ScheduleRemoveGame(gameState.GameId, _computerGameTimeout);
return Ok(new
{
@@ -96,7 +103,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
isWhite = false;
}
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
return Ok(new
{
@@ -158,7 +165,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
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);
@@ -167,7 +174,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
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);
@@ -175,18 +182,11 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
return BadRequest(result);
if (result.IsCheckmate || result.IsStalemate)
{
// queue game removal
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(1));
}
ScheduleRemoveGame(gameState.GameId, _gameCleanupTimeout);
else if (gameState.IsVsComputer)
ScheduleRemoveGame(gameState.GameId, _computerGameTimeout);
else
{
// increase timeout if play continues.
if (gameState.IsVsComputer)
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
else
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
}
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
if (gameState.IsVsComputer && gameState.Computer is not null)
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
@@ -229,31 +229,36 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
private static void ScheduleRemoveGame(Guid id, TimeSpan delay)
{
if (_gameRemovalTasks.ContainsKey(id))
if (_gameRemovalCancellationTokens.TryRemove(id, out var oldCts))
{
oldCts.Cancel();
oldCts.Dispose();
}
var cts = new CancellationTokenSource();
_gameRemovalCancellationTokens[id] = cts;
_gameRemovalTasks[id] = Task.Run(async () =>
{
await Task.Delay(delay);
if (_games[id].Computer is not null)
await _games[id].Computer!.DisposeAsync();
_games.Remove(id, out _);
_gameRemovalTasks.Remove(id, out _);
});
return;
}
_gameRemovalTasks.TryAdd(id, Task.Run(async () =>
try
{
await Task.Delay(delay);
await Task.Delay(delay, cts.Token);
if (_games[id].Computer is not null)
await _games[id].Computer!.DisposeAsync();
if (_games.TryGetValue(id, out var game) && game.Computer is not null)
await game.Computer.DisposeAsync();
_games.Remove(id, out _);
_gameRemovalTasks.Remove(id, out _);
}));
}
catch (OperationCanceledException) { }
finally
{
if (_gameRemovalCancellationTokens.TryGetValue(id, out var currentCts) && currentCts == cts)
{
_gameRemovalCancellationTokens.TryRemove(id, out _);
}
cts.Dispose();
}
});
}
}
+1
View File
@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>53ed685c-bdff-4306-8cc2-9fbe55c85713</UserSecretsId>
</PropertyGroup>
<ItemGroup>
+8 -1
View File
@@ -59,7 +59,14 @@
@section Scripts {
<script src="~/js/signalr/signalr.min.js"></script>
<script src="~/js/ChessScripts/chessLogic.js"></script>
<script src="~/js/ChessScripts/GameState.js"></script>
<script src="~/js/ChessScripts/ChessUtils.js"></script>
<script src="~/js/ChessScripts/ChessAPI.js"></script>
<script src="~/js/ChessScripts/ChessSignalR.js"></script>
<script src="~/js/ChessScripts/ChessInteractions.js"></script>
<script src="~/js/ChessScripts/ChessBoard.js"></script>
<script src="~/js/ChessScripts/ChessModals.js"></script>
<script src="~/js/ChessScripts/chessMain.js"></script>
}
@section Styles {
+10 -4
View File
@@ -16,14 +16,14 @@
<div id="ProjectsBox">
<div id="ChessProject" class="projectContainer">
<div class="displayBox diagonal-section-left">
<a target="_blank" href="https://github.com/JoshHeaps/OnlineChess">
<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. 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.
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>
@@ -49,8 +49,14 @@
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/chess'">
Play Chess
</button>
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/particles'">
Life
<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...
+54
View File
@@ -0,0 +1,54 @@
@page
@model JoshHeaps.Net.Pages.MemoryLaneModel
@{
ViewData["Title"] = "Memory Lane";
Layout = "_Layout";
}
<div class="memory-lane-container">
<h1 class="memory-lane-header">Memory Lane</h1>
<p class="memory-lane-subtitle">Special moments and celebrations from our life together</p>
<div class="events-grid">
<a href="/memoryLane/fifthTempleAnniversary" class="event-card">
<div class="event-icon">💝</div>
<div class="event-title">Our 5th Sealing Anniversary</div>
<div class="event-description">
Celebrating the fifth year of our eternal journey together
</div>
</a>
<div class="event-card disabled">
<div class="event-icon">🎂</div>
<div class="event-title">Birthdays & Births</div>
<div class="event-description">
Welcoming new life and celebrating another year
</div>
<div class="coming-soon">Coming soon...</div>
</div>
<div class="event-card disabled">
<div class="event-icon">🌟</div>
<div class="event-title">Milestones</div>
<div class="event-description">
Big achievements and unforgettable moments
</div>
<div class="coming-soon">Coming soon...</div>
</div>
<div class="event-card disabled">
<div class="event-icon">✨</div>
<div class="event-title">More Memories</div>
<div class="event-description">
Even more special occasions and celebrations
</div>
<div class="coming-soon">Coming soon...</div>
</div>
</div>
<a href="/" class="home-button">← Back to Home</a>
</div>
@section Styles {
<link href="/css/memoryLane/page.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
}
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages
{
public class MemoryLaneModel() : PageModel
{
public void OnGet()
{
}
}
}
@@ -0,0 +1,69 @@
@page
@model JoshHeaps.Net.Pages.MemoryLane.FifthTempleAnniversary
@{
ViewData["Title"] = "Happy 5th Anniversary, Morgan!";
Layout = "_Layout";
}
<div class="anniversary-container">
<h1 class="anniversary-header">Happy 5th Anniversary, Morgan!</h1>
<div id="WelcomeMessage"></div>
<div class="hearts">💖 💕 💗</div>
<div class="photo-gallery">
<div class="photo-frame">
<div class="photo-wrapper">
<img src="/images/Wife Images/45053.jpeg" alt="Morgan with our beautiful baby">
</div>
<div class="photo-caption">The most amazing mother to our precious child</div>
</div>
<div class="photo-frame">
<div class="photo-wrapper">
<img src="/images/Wife Images/45054.jpeg" alt="Josh and Morgan together">
</div>
<div class="photo-caption">My best friend and eternal companion</div>
</div>
</div>
<div class="love-message">
<h2>Five Years of Forever</h2>
<p>
Five years ago today, on October 2nd, 2020, I knelt across the altar from you in the temple
and we were sealed for time and all eternity. It was the best decision I ever made.
</p>
<p>
Morgan, you are everything I could have ever hoped for and so much more. You're not just
my wife—you're my best friend, my partner in every adventure, and the love of my life.
</p>
<p>
You make me laugh every single day. Your kindness touches everyone around you. Your
strength amazes me. Your beauty, inside and out, takes my breath away. Watching you
become a mother has shown me depths of love and dedication I never knew existed.
</p>
<p>
Thank you for five incredible years of love, laughter, growth, and joy. Thank you for
choosing me, for believing in me, and for building this beautiful life with me.
</p>
<p>
Here's to five years down and an eternity to go. I love you more today than yesterday,
and I'll love you even more tomorrow.
</p>
<p style="font-size: 1.5rem; margin-top: 2rem; color: #764ba2;">
<strong>I love you, Morgan. Forever and always. 💖</strong>
</p>
</div>
<div class="hearts">💝 💞 💓</div>
</div>
@section Styles {
<link href="/css/anniversary/page.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
}
@section Scripts {
<script src="/js/anniversary/page.js"></script>
<script src="/js/utils.js"></script>
}
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages.MemoryLane
{
public class FifthTempleAnniversary() : PageModel
{
public void OnGet()
{
}
}
}
@@ -1,35 +1,30 @@
namespace JoshHeaps.Net.Services.Implementations;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace JoshHeaps.Net.Services.Implementations;
public class AutoIpUpdateService(
IConfiguration config,
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");
@@ -37,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
{
@@ -52,7 +69,7 @@ public class AutoIpUpdateService(
}
}
private async Task<AAAARecord> GetDnsRecordAsync()
private async Task<RecordList> GetDnsRecordAsync()
{
try
{
@@ -61,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
{
@@ -73,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)
{
@@ -99,7 +117,23 @@ public class AutoIpUpdateService(
}
}
record AAAARecord(string Comment, string Content, string Name, string Id);
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("proxied")] bool Proxied,
[property: JsonPropertyName("id")] string Id);
record RecordList(List<AAAARecord> Result);
record RecordList([property: JsonPropertyName("result")] List<DnsRecord> Records)
{
public DnsRecord this[int index]
{
get
{
return Records[index];
}
}
}
}
@@ -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,218 @@
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
padding: 0;
}
.anniversary-container {
max-width: 1400px;
margin: 0 auto;
padding: 3rem 2rem;
text-align: center;
}
.anniversary-header {
color: white;
font-size: 3.5rem;
margin-bottom: 1.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
animation: fadeInDown 1s ease-out;
}
#WelcomeMessage {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 2rem 2.5rem;
margin: 0 auto 2rem;
max-width: 900px;
font-size: 1.3rem;
line-height: 1.8;
color: #333;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
animation: fadeIn 2s ease-out;
}
.hearts {
font-size: 2.5rem;
margin: 1.5rem 0;
animation: heartbeat 1.5s ease-in-out infinite;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.photo-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 2rem;
margin: 2rem auto;
max-width: 1200px;
padding: 0 1rem;
align-items: start;
}
.photo-frame {
background: white;
padding: 1rem;
border-radius: 15px;
box-shadow: 0 15px 40px rgba(0,0,0,0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
animation: fadeInUp 1.5s ease-out;
display: flex;
flex-direction: column;
}
.photo-frame:hover {
transform: translateY(-10px);
box-shadow: 0 20px 50px rgba(0,0,0,0.4);
}
.photo-wrapper {
width: 100%;
height: 500px;
overflow: hidden;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
}
.photo-frame img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.photo-frame:nth-child(2) img {
object-position: center 30%;
}
.photo-caption {
margin-top: 1rem;
font-style: italic;
color: #666;
font-size: 1.1rem;
padding: 0.5rem;
}
.love-message {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 2.5rem 3rem;
margin: 2rem auto;
max-width: 1000px;
color: #444;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
animation: fadeIn 2.5s ease-out;
}
.love-message h2 {
color: #764ba2;
margin-bottom: 1.5rem;
font-size: 2.5rem;
}
.love-message p {
font-size: 1.2rem;
line-height: 1.9;
margin-bottom: 1.2rem;
text-align: left;
}
.love-message p:last-of-type {
text-align: center;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes heartbeat {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@media (max-width: 900px) {
.photo-gallery {
grid-template-columns: 1fr;
max-width: 600px;
}
.photo-wrapper {
height: 450px;
}
}
@media (max-width: 768px) {
.anniversary-container {
padding: 2rem 1rem;
}
.anniversary-header {
font-size: 2.2rem;
}
#WelcomeMessage {
font-size: 1.1rem;
padding: 1.5rem;
}
.photo-wrapper {
height: 400px;
}
.love-message {
padding: 1.5rem;
}
.love-message h2 {
font-size: 1.8rem;
}
.love-message p {
font-size: 1rem;
}
.hearts {
font-size: 2rem;
}
}
+32 -1
View File
@@ -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;
}
+2 -2
View File
@@ -55,8 +55,8 @@
text-align: center;
border: none;
border-radius: 5vw;
width: 25vw;
height: 10vw;
width: 20vw;
height: 8vw;
font-size: 2vw;
background-color: rgb(104, 255, 0, 0.39);
cursor: pointer;
@@ -0,0 +1,162 @@
body {
background: #1a1a1a;
min-height: 100vh;
margin: 0;
padding: 0;
}
.memory-lane-container {
max-width: 1000px;
margin: 0 auto;
padding: 4rem 2rem;
text-align: center;
}
.memory-lane-header {
color: #f5f5f5;
font-size: 2.5rem;
margin-bottom: 0.5rem;
font-weight: 300;
letter-spacing: 0.1em;
}
.memory-lane-subtitle {
color: #a0a0a0;
font-size: 1rem;
margin-bottom: 4rem;
font-weight: 300;
}
.events-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin: 3rem auto;
max-width: 900px;
padding: 0 1rem;
}
.event-card {
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 4px;
padding: 2rem 1.5rem;
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
align-items: center;
}
.event-card:hover {
background: #2f2f2f;
border-color: #4a4a4a;
transform: translateY(-2px);
}
.event-card.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.event-card.disabled:hover {
background: #2a2a2a;
border-color: #3a3a3a;
transform: none;
}
.event-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.event-title {
font-size: 1.2rem;
font-weight: 400;
color: #f5f5f5;
margin-bottom: 0.75rem;
letter-spacing: 0.05em;
}
.event-description {
font-size: 0.9rem;
color: #999;
line-height: 1.6;
font-weight: 300;
}
.coming-soon {
color: #666;
font-style: italic;
margin-top: 0.5rem;
font-size: 0.85rem;
}
.home-button {
margin-top: 4rem;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
background: transparent;
color: #f5f5f5;
border: 1px solid #3a3a3a;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
letter-spacing: 0.05em;
}
.home-button:hover {
background: #2a2a2a;
border-color: #4a4a4a;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.memory-lane-header {
font-size: 2.5rem;
}
.memory-lane-subtitle {
font-size: 1.1rem;
}
.events-grid {
grid-template-columns: 1fr;
}
.event-icon {
font-size: 3rem;
}
.event-title {
font-size: 1.6rem;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@@ -0,0 +1,113 @@
const ChessAPI = {
async joinGame() {
const response = await fetch('/api/chess/JoinGame');
return await response.json();
},
async createCPUGame(difficulty) {
const response = await fetch(`/api/chess/new/${difficulty}`);
return await response.json();
},
async getGameState(gameId) {
const response = await fetch(`/api/chess/${gameId}`);
return await response.json();
},
async getLegalMoves(pieceId) {
const response = await fetch(`/api/chess/${GameState.currentGameId}/legalMoves/${pieceId}`);
if (!response.ok) throw new Error("API failed");
return await response.json();
},
async makeMove(moveDto) {
const response = await fetch("/api/chess/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
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 (!result.success) {
throw new Error(result.message || "Invalid move or not your turn.");
}
return result;
},
async handleMove(targetRow, targetCol) {
if (!GameState.selectedPiece) return;
const isPawn = GameState.selectedPiece.type === 0;
const reachedEnd = (Number(GameState.selectedPiece.color) === 0 && Number(targetRow) === 0) ||
(Number(GameState.selectedPiece.color) === 1 && Number(targetRow) === 0);
let choice = null;
if (isPawn && reachedEnd) {
choice = await ChessModals.promptPromotion();
if (!choice) return;
}
[targetRow, targetCol] = ChessUtils.flipCoordinates(targetRow, targetCol);
const moveDto = {
GameId: GameState.currentGameId,
PlayerId: GameState.currentPlayerId,
PieceId: GameState.selectedPiece.id,
SourceRow: GameState.selectedPiece.row,
SourceCol: GameState.selectedPiece.col,
TargetRow: targetRow,
TargetCol: targetCol,
PromotionChoice: choice
};
GameState.setPreviousMove([GameState.selectedPiece.row, GameState.selectedPiece.col], [targetRow, targetCol]);
try {
const moveResult = await this.makeMove(moveDto);
const updatedGame = await this.getGameState(GameState.currentGameId);
ChessBoard.renderPieces(updatedGame.pieces);
GameState.clearSelection();
ChessInteractions.clearHighlights();
await ChessSignalR.notifyMoveMade(moveDto, moveResult);
this.alertGameStatusChange(moveResult);
} catch (error) {
alert("❌ " + error.message);
}
},
alertGameStatusChange(moveResult) {
setTimeout(async () => {
let gameOver = false;
if (moveResult.isCheck) {
console.log("🛑 Check!");
}
if (moveResult.isCheckmate) {
alert("♟️ Checkmate!");
gameOver = true;
} else if (moveResult.isStalemate) {
alert("🤝 Stalemate!");
gameOver = true;
}
if (gameOver) {
await ChessSignalR.leaveGame();
}
}, 500);
}
};
console.log("ChessAPI.js loaded");
@@ -0,0 +1,146 @@
const ChessBoard = {
renderPieces(pieces) {
this.clearAllSquares();
this.renderCoordinateLabels();
this.placePieces(pieces);
this.setupSquareEventHandlers();
this.highlightPreviousMove();
},
clearAllSquares() {
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
square.innerHTML = "";
}
},
placePieces(pieces) {
pieces.forEach(piece => {
const [r, c] = ChessUtils.flipCoordinates(piece.row, piece.col);
const index = r * 8 + c;
const square = document.getElementById(`square-${index}`);
if (!square) return;
const img = document.createElement("img");
img.src = ChessUtils.getPieceImageUrl(piece);
img.alt = piece.type;
img.classList.add("chessPiece");
this.setupPieceDragEvents(img, piece);
this.setupPieceClickEvent(img, piece);
square.appendChild(img);
});
},
setupPieceDragEvents(img, piece) {
img.draggable = true;
img.ondragstart = async (e) => {
GameState.setSelectedPiece(piece);
try {
const moves = await ChessAPI.getLegalMoves(piece.id);
GameState.setLegalMoves(moves);
ChessInteractions.highlightSelected(piece.row, piece.col);
ChessInteractions.highlightLegalMoves(moves);
} catch (err) {
console.error(err);
}
e.dataTransfer.setData("text/plain", JSON.stringify({
srcRow: piece.Row,
srcCol: piece.Col
}));
};
img.ondragend = () => ChessInteractions.clearHighlights();
},
setupPieceClickEvent(img, piece) {
img.onclick = (e) => {
e.stopPropagation();
ChessInteractions.handlePieceClick(piece);
console.log("Clicked on:", piece);
};
},
setupSquareEventHandlers() {
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
square.classList.remove("previous-start", "previous-end");
if (!square.classList.contains("legal")) {
square.onclick = () => ChessInteractions.clearHighlights();
}
}
},
highlightPreviousMove() {
if (GameState.previousMoveStart && GameState.previousMoveEnd) {
const [startRow, startCol] = ChessUtils.flipCoordinates(...GameState.previousMoveStart);
const [endRow, endCol] = ChessUtils.flipCoordinates(...GameState.previousMoveEnd);
const startIndex = startRow * 8 + startCol;
const endIndex = endRow * 8 + endCol;
document.getElementById(`square-${startIndex}`)?.classList.add("previous-start");
document.getElementById(`square-${endIndex}`)?.classList.add("previous-end");
}
},
setupDragAndDrop() {
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
square.ondragover = (e) => {
e.preventDefault();
};
square.ondrop = (e) => {
e.preventDefault();
if (!GameState.selectedPiece) return;
const index = parseInt(square.id.split('-')[1], 10);
const targetRow = Math.floor(index / 8);
const targetCol = index % 8;
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);
}
}
}
};
console.log("ChessBoard.js loaded");
@@ -0,0 +1,59 @@
const ChessInteractions = {
async handlePieceClick(piece) {
this.clearHighlights();
GameState.setSelectedPiece(piece);
try {
const moves = await ChessAPI.getLegalMoves(piece.id);
GameState.setLegalMoves(moves);
this.highlightSelected(piece.row, piece.col);
this.highlightLegalMoves(moves);
} catch (err) {
console.error("Error in handlePieceClick:", err);
}
},
highlightSelected(row, col) {
const [r, c] = ChessUtils.flipCoordinates(row, col);
const index = r * 8 + c;
document.getElementById(`square-${index}`).classList.add("selected");
},
highlightLegalMoves(moves) {
moves.forEach(move => {
const [r, c] = ChessUtils.flipCoordinates(move.row, move.col);
const index = r * 8 + c;
const square = document.getElementById(`square-${index}`);
square.classList.add("legal");
const img = square.querySelector("img");
if (img) img.onclick = null;
let overlay = document.createElement("div");
overlay.className = "legalOverlay";
square.appendChild(overlay);
square.onclick = () => ChessAPI.handleMove(r, c);
});
},
clearHighlights() {
for (let i = 0; i < 64; i++) {
const square = document.getElementById(`square-${i}`);
const overlay = square.querySelector(".legalOverlay");
if (overlay) square.removeChild(overlay);
if (square.classList.contains("legal")) {
square.classList.remove("legal");
square.onclick = null;
}
square.classList.remove("selected");
}
GameState.clearSelection();
}
};
console.log("ChessInteractions.js loaded");
@@ -0,0 +1,29 @@
const ChessModals = {
promptPromotion() {
return new Promise(resolve => {
let color = 1;
if (GameState.currentPlayerIsWhite)
color = 0;
ChessUtils.updatePromotionModalImages(color);
document.getElementById("promotionModal").style.display = "block";
window.selectPromotion = (piece) => {
document.getElementById("promotionModal").style.display = "none";
resolve(piece);
};
});
},
promptDifficulty() {
return new Promise(resolve => {
document.getElementById("difficultyModal").style.display = "block";
window.selectDifficulty = (difficulty) => {
document.getElementById("difficultyModal").style.display = "none";
resolve(difficulty);
};
});
}
};
console.log("ChessModals.js loaded");
@@ -0,0 +1,59 @@
const ChessSignalR = {
connection: null,
async setupConnection() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/chessHub")
.configureLogging(signalR.LogLevel.Information)
.build();
this.connection.onclose(err => {
console.error("❌ SignalR connection closed:", err?.message);
});
this.connection.on("ReceiveMoveUpdate", async (gameId, moveDto, moveResultDto) => {
await this.handleMoveUpdate(gameId, moveDto, moveResultDto);
});
try {
await this.connection.start();
console.log("✅ SignalR connected");
await this.connection.invoke("JoinWebsocketGroup", GameState.currentGameId);
} catch (err) {
console.error("❌ SignalR failed to start or join:", err);
}
},
async handleMoveUpdate(gameId, moveDto, moveResultDto) {
if (gameId !== GameState.currentGameId) return;
const gameState = await ChessAPI.getGameState(gameId);
GameState.setPreviousMove([moveDto.sourceRow, moveDto.sourceCol], [moveDto.targetRow, moveDto.targetCol]);
ChessBoard.renderPieces(gameState.pieces);
ChessAPI.alertGameStatusChange(moveResultDto);
},
async notifyMoveMade(moveDto, moveResult) {
if (this.connection) {
await this.connection.invoke("MoveMade", GameState.currentGameId, moveDto, moveResult);
}
},
async leaveGame() {
if (this.connection) {
await this.connection.invoke("LeaveWebsocketGroup", GameState.currentGameId);
await this.connection.stop();
this.connection = null;
}
},
async stopConnection() {
if (this.connection) {
await this.connection.stop();
this.connection = null;
}
}
};
console.log("ChessSignalR.js loaded");
@@ -0,0 +1,44 @@
const ChessUtils = {
flipCoordinates(row, col) {
if (!GameState.currentPlayerIsWhite) {
return [7 - row, 7 - col];
}
return [row, col];
},
getCookie(name) {
const value = document.cookie.split('; ')
.find(row => row.startsWith(name + '='));
return value ? value.split('=')[1] : null;
},
setCookie(name, value, maxAge = 86400) {
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
},
getPieceImageUrl(piece) {
const basePath = "/images/Chess Images/";
const color = piece.color === 0 ? "White" : "Black";
const typeMap = {
0: "Pawn",
1: "Rook",
2: "Knight",
3: "Bishop",
4: "Queen",
5: "King"
};
return basePath + color + typeMap[piece.type] + ".svg";
},
updatePromotionModalImages(color) {
const pieceNames = ["Queen", "Rook", "Bishop", "Knight"];
const buttons = document.querySelectorAll("#promotionModal button img");
buttons.forEach((img, index) => {
img.src = `/images/Chess Images/${color === 0 ? "White" : "Black"}${pieceNames[index]}.svg`;
});
}
};
console.log("ChessUtils.js loaded");
@@ -0,0 +1,50 @@
const GameState = {
currentGameId: null,
currentPlayerId: null,
currentPlayerIsWhite: null,
selectedPiece: null,
legalMoves: [],
previousMoveStart: null,
previousMoveEnd: null,
reset() {
this.currentGameId = null;
this.currentPlayerId = null;
this.currentPlayerIsWhite = null;
this.selectedPiece = null;
this.legalMoves = [];
this.previousMoveStart = null;
this.previousMoveEnd = null;
},
setGameInfo(gameId, playerId, isWhite) {
this.currentGameId = gameId;
this.currentPlayerId = playerId;
this.currentPlayerIsWhite = isWhite;
},
setSelectedPiece(piece) {
this.selectedPiece = piece;
},
clearSelection() {
this.selectedPiece = null;
this.legalMoves = [];
},
setLegalMoves(moves) {
this.legalMoves = moves;
},
setPreviousMove(startPos, endPos) {
this.previousMoveStart = startPos;
this.previousMoveEnd = endPos;
},
clearPreviousMove() {
this.previousMoveStart = null;
this.previousMoveEnd = null;
}
};
console.log("GameState.js loaded");
@@ -0,0 +1,82 @@
async function startNewGame() {
await ChessSignalR.stopConnection();
try {
const gameData = await ChessAPI.joinGame();
GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
GameState.clearPreviousMove();
ChessUtils.setCookie('chessGameId', gameData.gameId);
ChessUtils.setCookie('chessPlayerId', gameData.id);
ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
console.log("🆕 Game started:", gameData.gameId);
await ChessSignalR.setupConnection();
const gameState = await ChessAPI.getGameState(gameData.gameId);
ChessBoard.renderPieces(gameState.pieces);
} catch (error) {
console.error("Failed to start new game:", error);
}
}
async function startCPUGame() {
await ChessSignalR.stopConnection();
try {
const difficulty = await ChessModals.promptDifficulty();
const gameData = await ChessAPI.createCPUGame(difficulty);
GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
GameState.clearPreviousMove();
ChessUtils.setCookie('chessGameId', gameData.gameId);
ChessUtils.setCookie('chessPlayerId', gameData.id);
ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
console.log("🆕 CPU Game started:", gameData.gameId);
await ChessSignalR.setupConnection();
const gameState = await ChessAPI.getGameState(gameData.gameId);
ChessBoard.renderPieces(gameState.pieces);
} catch (error) {
console.error("Failed to start CPU game:", error);
}
}
async function resumeSavedGame() {
const savedGameId = ChessUtils.getCookie("chessGameId");
const savedPlayerId = ChessUtils.getCookie("chessPlayerId");
const savedPlayerIsWhite = ChessUtils.getCookie("chessPlayerIsWhite");
if (savedGameId && savedPlayerId) {
try {
const gameState = await ChessAPI.getGameState(savedGameId);
console.log("🧠 Rejoining saved game...");
GameState.setGameInfo(savedGameId, savedPlayerId, savedPlayerIsWhite === "true");
await ChessSignalR.setupConnection();
ChessBoard.renderPieces(gameState.pieces);
} catch (err) {
console.warn("Saved game not found or expired.", err);
}
}
}
window.startNewGame = startNewGame;
window.startCPUGame = startCPUGame;
window.addEventListener('load', resumeSavedGame);
document.addEventListener('DOMContentLoaded', () => {
ChessBoard.setupDragAndDrop();
});
console.log("chessMain.js loaded");
+16
View File
@@ -0,0 +1,16 @@
document.addEventListener('DOMContentLoaded', () => {
getIpStatus();
});
async function getIpStatus() {
try {
const res = await fetch(`/api/debug/IpCheck`);
if (!res.ok) throw new Error("API failed");
const text = await res.text();
console.log(text);
document.querySelector('#IsIpChecking').textContent = text;
} catch (err) {
console.error("Error in api:", err);
}
}
@@ -0,0 +1,7 @@
window.onload = function () {
const element = document.getElementById("WelcomeMessage");
element.style.display = "none"; // Trigger reflow
element.offsetHeight; // Force reflow
element.style.display = ""; // Restore the original display
typeText("#WelcomeMessage", "Today, October 2nd, marks the 5th anniversary of the day I was sealed for time and all eternity to my wonderful wife. She's the kindest, sweetest, funniest, most beautiful woman I've ever met. I love you Morgan 💖");
}
+1 -1
View File
@@ -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! Since I have your attention, why don't I tell you 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?");
}
+11 -7
View File
@@ -12,23 +12,27 @@
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) {
intervalId = setInterval(() => {
if (index < text.length) {
if (text.charAt(index) === '`')
currentText = currentText.slice(0, -1);
else
currentText += text.charAt(index);
changeIntervalTime(element, text);
index++;