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
This commit is contained in:
DJ Gillespie 2026-02-02 16:29:54 -07:00
parent 35f5a6391d
commit b00f41ac80
3 changed files with 147 additions and 4 deletions

View File

@ -102,6 +102,11 @@
<div id="product-info"></div>
</div>
<div id="game-stats"></div>
<div id="navigation-container">
<button id="prev-button" class="nav-button">← Previous</button>
<span id="day-indicator"></span>
<button id="next-button" class="nav-button">Next →</button>
</div>
<div id="guesses-container">
<div id="warning-toast" class="animate__animated hide">
<center>Invalid Guess ⚠️</center>

View File

@ -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();
}
}

View File

@ -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;
}