// ==UserScript==
// @name NYTXW Add Prev Next Buttons
// @namespace https://github.com/seeshanty
// @version 2025-03-31
// @description Add Previous, 📆, and Next buttons to the New York Times Crossword Puzzle webpage for easier navigation around the calendar
// @author seeshanty
// @license CC0-1.0
// @supportURL https://github.com/seeshanty/nytxw_buttons
// @match https://www.nytimes.com/crosswords/game/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=nytimes.com
// @grant none
// ==/UserScript==
// TODO: Handle bonus puzzles somehow; URL format below
// https://www.nytimes.com/crosswords/game/bonus/2024/02
// ⭐
(function() {
'use strict';
// BOOKMARK FUNCTIONALITY FOR WORKING THROUGH THE ARCHIVE FROM MAIN /daily PUZZLE (LATEST PUZZLE)
// When set to true, the calendar button on the latest puzzle will send you to whatever month you set here
const useArchiveBookmark = true;
const archiveBookmarkYear = 2024;
const archiveBookmarkMonth = 9;
// STYLE PARAMETERS
const navBtnWidth = "100px";
const navBtnSpacing = "10px";
const navBtnBorder = "1px solid black";
const calBtnWidth = "40px";
const calBtnBorder = "1px solid white";
const btnColor = "white";
const btnHoverColor = "lightgray";
const btnLocation = "navBar"; // Options: navBar, crosswordDate
// HARDCODED BASE URLS - these must end in "/" for proper URL concatenation later
const baseUrl = "https://www.nytimes.com/crosswords/game/daily/"; // default daily puzzle page
const archiveBaseUrl = "https://www.nytimes.com/crosswords/archive/daily/";
// FUNCTIONALITY PARAMETERS
const extraTimeout = 200; // increase this if the script isn't catching the correct place to put the buttons
// INITIALIZE VARIABLES
var currentPuzzleUrlDate = "";
var currentPuzzleDate = new Date();
var currentMonthArchiveUrl = "";
var isLatestPuzzle = false;
var targetArchiveUrl = "";
//==========================================================================
// THINGS WE CAN DO WHILE WINDOW IS LOADING
//==========================================================================
// Get the current URL
var currentUrl = window.location.href;
console.log("Current URL:", currentUrl);
if (currentUrl.endsWith('/')) {
currentUrl = currentUrl.slice(0, -1);
console.log("Removed trailing slash in URL.\nUpdated URL:", currentUrl);
}
// Figure out what the date is for the latest puzzle available
//---------------------------------------------------------------------
// New puzzles are available at 10 PM ET M-F and 6 PM S/S (NY timezone)
// set variables for the current date/time and date/time in NY
const localDate = new Date();
const dateInNY = new Date(localDate.toLocaleString("en-US", { timeZone: "America/New_York" }));
console.log("dateInNY:",dateInNY);
// Use date parsing to get the other needed date components (year, month, day, hour)
const NY_YYYY = dateInNY.getFullYear();
const NY_MM = String(dateInNY.getMonth() + 1).padStart(2, '0'); // Add 1 because January is month 0
const NY_DD = String(dateInNY.getDate()).padStart(2, '0');
const NY_HH24 = String(dateInNY.getHours()).padStart(2, '0');
const NY_DAY = dateInNY.getDay();
const NY_isWeekend = [0,6].includes(NY_DAY); // 0 = Saturday, 6 = Sunday
console.log("Parsed date elements","\n ",
"NY_YYYY:", NY_YYYY,"\n ",
"NY_MM:", NY_MM,"\n ",
"NY_DD:", NY_DD,"\n ",
"NY_HH24:", NY_HH24,"\n ",
"NY_DAY:", NY_DAY,"\n ",
"NY_isWeekend:", NY_isWeekend);
// Make a date object for the latest puzzle available
// Start by assuming the date for the latest puzzle matches the date in NY
const latestPuzzleDate = new Date(dateInNY);
if (NY_isWeekend && NY_HH24 >= 18 && NY_HH24 < 24) { // it's a weekend and it's between 6 PM and midnight
console.log("New weekend puzzle should be available.");
latestPuzzleDate.setDate(latestPuzzleDate.getDate() + 1);
} else if (NY_HH24 >= 22 && NY_HH24 < 24) { // not a weekend; between 10 PM and midnight
console.log("New weekday puzzle should be available.");
latestPuzzleDate.setDate(latestPuzzleDate.getDate() + 1);
} else {
console.log("Latest puzzle matches current date in NY.");
}
console.log("latestPuzzleDate:", latestPuzzleDate);
// It's useful to have pre-formatted strings matching the URLs for the puzzle
const latestPuzzleUrlDate = latestPuzzleDate.getFullYear() + '/' +
String(latestPuzzleDate.getMonth() + 1).padStart(2, '0') + '/' +
String(latestPuzzleDate.getDate()).padStart(2, '0');
const latestMonthArchiveUrl = latestPuzzleDate.getFullYear() + '/' +
String(latestPuzzleDate.getMonth() + 1).padStart(2, '0');
console.log("latestPuzzleUrlDate:",latestPuzzleUrlDate);
console.log("latestMonthArchiveUrl:",latestMonthArchiveUrl);
// Figure out whether we are on the latest puzzle available
//---------------------------------------------------------------------
// Parse puzzle date from URL
if (currentUrl == baseUrl.slice(0, -1)) {
//There is no YYYY/MM/DD in the URL; we're on the daily page
console.log("No date in URL. Assuming puzzle is newest puzzle.");
currentPuzzleUrlDate = latestPuzzleUrlDate;
currentPuzzleDate = new Date(latestPuzzleDate);
currentMonthArchiveUrl = latestMonthArchiveUrl;
isLatestPuzzle = true;
} else {
// Extract the baseUrl, year, month, and day from the URL
// Assumption: puzzle URLs end in /YYYY/MM/DD
const parts = currentUrl.split('/');
// baseUrl = parts.slice(0, parts.length - 3).join('/') + '/';
const URL_YYYY = parts[parts.length - 3];
const URL_MM = parts[parts.length - 2];
const URL_DD = parts[parts.length - 1];
currentPuzzleUrlDate = URL_YYYY + "/" + URL_MM + "/" + URL_DD;
currentPuzzleDate = new Date(URL_YYYY, URL_MM - 1, URL_DD);
currentMonthArchiveUrl = URL_YYYY + "/" + String(URL_MM).padStart(2, '0');
console.log("URL Date: ",currentPuzzleUrlDate,"\n",currentPuzzleDate);
// Check if puzzleDate is the same day as the latest available puzzle
isLatestPuzzle = latestPuzzleDate.getFullYear() === currentPuzzleDate.getFullYear() &&
latestPuzzleDate.getMonth() === currentPuzzleDate.getMonth() &&
latestPuzzleDate.getDate() === currentPuzzleDate.getDate();
} // puzzle date parsing
console.log("isLatestPuzzle:", isLatestPuzzle);
//----------------
// PREVIOUS
//----------------
// Calculate the previous date
const previousDate = new Date(currentPuzzleDate);
previousDate.setDate(currentPuzzleDate.getDate() - 1);
console.log("Previous Date:", previousDate);
// Format the previous date as YYYY/MM/DD
const previousUrlDate = previousDate.getFullYear() + '/' +
String(previousDate.getMonth() + 1).padStart(2, '0') + '/' +
String(previousDate.getDate()).padStart(2, '0');
// Concatenate previousUrl
const previousUrl = baseUrl + previousUrlDate;
console.log("Previous URL:", previousUrl);
//----------------
// NEXT
//----------------
// Calculate the next date
const nextDate = new Date(currentPuzzleDate);
nextDate.setDate(currentPuzzleDate.getDate() + 1);
console.log("Next Date:", nextDate);
// Format the next date as YYYY/MM/DD
const nextUrlDate = nextDate.getFullYear() + '/' +
String(nextDate.getMonth() + 1).padStart(2, '0') + '/' +
String(nextDate.getDate()).padStart(2, '0');
// Concatenate nextUrl
const nextUrl = baseUrl + nextUrlDate;
console.log("Next URL:", nextUrl);
//-------------------
// ARCHIVE CALENDAR
//-------------------
// Archive Calendar URL
if (isLatestPuzzle && useArchiveBookmark) {
targetArchiveUrl = archiveBookmarkYear + '/' + String(archiveBookmarkMonth).padStart(2, '0');
} else {
targetArchiveUrl = currentMonthArchiveUrl;
}
const archiveCalendarUrl = archiveBaseUrl + targetArchiveUrl;
console.log("Archive Calendar URL:", archiveCalendarUrl);
//--------------------------------------------------------------------------
// FUNCTIONS TO CREATE THE BUTTONS
//--------------------------------------------------------------------------
// Function to create the "Previous" button
function createPreviousButton() {
console.log("Creating the 'Previous' button...");
var previousBtn = document.createElement("button");
previousBtn.textContent = "Previous";
previousBtn.id = "previousBtn";
previousBtn.style.backgroundColor = btnColor;
previousBtn.style.width = navBtnWidth;
previousBtn.style.border= navBtnBorder;
previousBtn.addEventListener("mouseenter", function() {
previousBtn.style.backgroundColor = btnHoverColor;
});
previousBtn.addEventListener("mouseleave", function() {
previousBtn.style.backgroundColor = btnColor;
});
previousBtn.addEventListener("click", function() {
console.log("Previous button clicked...");
// Redirect to the previous URL
window.location.href = previousUrl;
});
console.log("Returning the 'Previous' button...");
return previousBtn;
}
// Function to create the "Next" button
function createNextButton() {
console.log("Creating the 'Next' button...");
var nextBtn = document.createElement("button");
nextBtn.textContent = "Next";
nextBtn.id = "nextBtn";
nextBtn.style.backgroundColor = btnColor;
nextBtn.style.width = navBtnWidth;
nextBtn.style.marginLeft = navBtnSpacing;
nextBtn.style.border= navBtnBorder;
if (isLatestPuzzle) {
nextBtn.disabled = true;
nextBtn.style.borderColor = "lightgray";
} else {
nextBtn.addEventListener("mouseenter", function() {
nextBtn.style.backgroundColor = btnHoverColor;
});
nextBtn.addEventListener("mouseleave", function() {
nextBtn.style.backgroundColor = btnColor;
});
nextBtn.addEventListener("click", function() {
console.log("Next button clicked...");
// Redirect to the next URL
window.location.href = nextUrl;
});
}
console.log("Returning the 'Next' button...");
return nextBtn;
}
function createArchiveCalendarButton() {
console.log("Creating the 'Archive Calendar' button...");
var archiveCalBtn = document.createElement("button");
archiveCalBtn.textContent = "📆";
archiveCalBtn.id = "archiveCalBtn";
archiveCalBtn.style.backgroundColor = btnColor;
archiveCalBtn.style.width = calBtnWidth;
archiveCalBtn.style.border= calBtnBorder;
archiveCalBtn.style.marginLeft = navBtnSpacing;
archiveCalBtn.addEventListener("mouseenter", function() {
archiveCalBtn.style.backgroundColor = btnHoverColor;
});
archiveCalBtn.addEventListener("mouseleave", function() {
archiveCalBtn.style.backgroundColor = btnColor;
});
archiveCalBtn.addEventListener("click", function() {
console.log("Archive Calendar button clicked...");
// Redirect to the archive calendar URL
window.location.href = archiveCalendarUrl;
});
console.log("Returning the 'Archive Calendar' button...");
return archiveCalBtn;
}
//--------------------------------------------------------------------------
// FUNCTION TO APPEND THE BUTTONS
//--------------------------------------------------------------------------
// Function to append the new navigation buttons to the specified element
function appendExtraNavButtons() {
var previousButton = createPreviousButton();
console.log("Previous Button:", previousButton);
var archiveCalButton = createArchiveCalendarButton();
console.log("Archive Calendar button:", archiveCalButton);
var nextButton = createNextButton();
console.log("Next Button:", nextButton);
// Possible targets:
// Crossword Date
var dateContainer = document.querySelector("div.xwd__details--date")
console.log("Date Container:", dateContainer);
// Nav bar
var navContainer = document.querySelector("#js-global-nav")
console.log("Nav Container:", navContainer);
// Set the target
var targetContainer = navContainer;
if (btnLocation == "crosswordDate") {
targetContainer = dateContainer;
}
console.log("Target Container:", targetContainer);
// Append the buttons
targetContainer.appendChild(previousButton);
console.log("Appended the 'Previous' button to the target container.");
targetContainer.appendChild(archiveCalButton);
console.log("Appended the 'Archive Calendar' button to the target container.");
targetContainer.appendChild(nextButton);
console.log("Appended the 'Next' button to the target container.");
}
//==========================================================================
// WAIT FOR THE WINDOW TO FINISH LOADING BEFORE ATTEMPTING TO APPEND BUTTONS
//==========================================================================
window.addEventListener('load', function() {
// need to give it a small timeout to make it work more reliably
setTimeout(function(){
appendExtraNavButtons();
}, extraTimeout);
}, false); // window load listener
})(); // main wrapper