From b00f41ac800dd695b4ee26e4214294c2846246be Mon Sep 17 00:00:00 2001 From: DJ Gillespie Date: Mon, 2 Feb 2026 16:29:54 -0700 Subject: [PATCH] Add archive navigation feature with URL parameter support - Add URL parameter parsing via getGameNumberFromURL() - Add dynamic MAX_GAME_NUMBER detection from games.json in order to properly handle page overflow - Add runtime validation with redirect for invalid game numbers - Implement Previous/Next navigation buttons - Add keyboard navigation (arrow keys) - Display game number and date in navigation - Update share URLs to include day parameter for archived games - Add navigation UI (HTML/CSS) matching project design --- index.html | 5 +++ scripts/main.js | 106 ++++++++++++++++++++++++++++++++++++++++++++++-- styles/game.css | 40 ++++++++++++++++++ 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 3e75d09..704b860 100644 --- a/index.html +++ b/index.html @@ -102,6 +102,11 @@
+
Invalid Guess ⚠️
diff --git a/scripts/main.js b/scripts/main.js index 2e76d56..a57c4b3 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -18,7 +18,29 @@ let warningTimeout; //The day Costcodle was launched. Used to find game number each day const costcodleStartDate = new Date("09/21/2023"); -const gameNumber = getGameNumber(); + +// Placeholder value until actual game count is loaded from games.json (10000 is chosen as it's +// safely beyond any realistic game count, allowing initial URL validation to pass through) +let MAX_GAME_NUMBER = 10000; + +// Get game number from URL parameter or use current date +function getGameNumberFromURL() { + const urlParams = new URLSearchParams(window.location.search); + const dayParam = urlParams.get('day'); + + if (dayParam) { + const requestedDay = parseInt(dayParam, 10); + // Validate the day is within available range + if (!isNaN(requestedDay) && requestedDay >= 1 && requestedDay <= MAX_GAME_NUMBER) { + return requestedDay; + } + } + + // Default to current day + return getGameNumber(); +} + +const gameNumber = getGameNumberFromURL(); //Elements with event listeners to play the game const input = document.getElementById("guess-input"); @@ -53,7 +75,7 @@ const gameState = JSON.parse(localStorage.getItem("state")) || { playGame(); function playGame() { - fetchGameData(getGameNumber()); + fetchGameData(gameNumber); } /* @@ -65,6 +87,19 @@ function fetchGameData(gameNumber) { fetch("./games.json") .then((response) => response.json()) .then((json) => { + // Dynamically determine max game number from JSON keys + if (MAX_GAME_NUMBER === 10000) { + const gameKeys = Object.keys(json).filter(key => key.startsWith('game-')); + MAX_GAME_NUMBER = gameKeys.length; + } + + // Validate that the requested game actually exists + if (!json[`game-${gameNumber}`]) { + console.warn(`Game #${gameNumber} not found, redirecting to current day`); + window.location.href = window.location.pathname; // Redirect to current day + return; + } + productName = json[`game-${gameNumber}`].name; productPrice = json[`game-${gameNumber}`].price; productPrice = Number(productPrice.slice(1, productPrice.length)); @@ -102,6 +137,9 @@ function initializeGame() { } else { convertToShareButton(); } + + // Initialize navigation buttons + initializeNavigation(); } function convertToShareButton() { @@ -244,18 +282,24 @@ function copyStats() { navigator.userAgent.match(/IEMobile/i) || navigator.userAgent.match(/Opera Mini/i); + // Calculate current day once for share URL logic + const currentDay = getGameNumber(); + const shareUrl = gameNumber !== currentDay + ? `https://costcodle.com/?day=${gameNumber}` + : "https://costcodle.com"; + if (isMobile) { if (navigator.canShare) { navigator .share({ title: "COSTCODLE", text: output, - url: "https://costcodle.com", + url: shareUrl, }) .catch((error) => console.error("Share failed:", error)); } } else { - output += `https://costcodle.com`; + output += shareUrl; navigator.clipboard.writeText(output); displayToast(); } @@ -513,3 +557,57 @@ function getGameNumber() { return Math.ceil(dayDifference) + 1; } +/* + Navigation functions for previous/next game +*/ + +function initializeNavigation() { + const prevButton = document.getElementById("prev-button"); + const nextButton = document.getElementById("next-button"); + const dayIndicator = document.getElementById("day-indicator"); + + // Verify elements exist before proceeding + if (!prevButton || !nextButton || !dayIndicator) { + console.error('Navigation elements not found'); + return; + } + + // Display current game number and date + const gameDate = new Date(costcodleStartDate.getTime() + (gameNumber - 1) * (1000 * 3600 * 24)); + dayIndicator.textContent = `Game #${gameNumber}`; + dayIndicator.title = gameDate.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric'}); + + // Disable buttons at boundaries + prevButton.disabled = gameNumber <= 1; + nextButton.disabled = gameNumber >= MAX_GAME_NUMBER; + + // Add event listeners + prevButton.addEventListener("click", navigateToPrevious); + nextButton.addEventListener("click", navigateToNext); + document.addEventListener("keydown", handleKeyboardNavigation); +} + +function navigateToPrevious() { + if (gameNumber > 1) { + window.location.href = `?day=${gameNumber - 1}`; + } +} + +function navigateToNext() { + if (gameNumber < MAX_GAME_NUMBER) { + window.location.href = `?day=${gameNumber + 1}`; + } +} + +function handleKeyboardNavigation(event) { + // Only handle arrow keys when not typing in input fields + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { + return; + } + + if (event.key === 'ArrowLeft') { + navigateToPrevious(); + } else if (event.key === 'ArrowRight') { + navigateToNext(); + } +} diff --git a/styles/game.css b/styles/game.css index c40f71c..60b3d0b 100644 --- a/styles/game.css +++ b/styles/game.css @@ -246,3 +246,43 @@ border-radius: 8px; background-color: rgb(0, 0, 0); } + +/* Navigation container */ +#navigation-container { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 16px; + margin-top: 8px; +} + +.nav-button { + font-family: "VT323", monospace; + font-size: 20px; + padding: 8px 16px; + color: white; + background-color: rgb(0, 96, 169); + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +.nav-button:hover:not(:disabled) { + background-color: rgba(0, 96, 169, 0.8); +} + +.nav-button:disabled { + background-color: rgb(173, 173, 173); + color: rgb(100, 100, 100); + cursor: not-allowed; +} + +#day-indicator { + font-family: "VT323", monospace; + font-size: 18px; + color: rgb(50, 50, 50); + min-width: 120px; + text-align: center; +}