Puzzmo Crossword Navigator

Navigate to most recent incomplete crossword

// ==UserScript==
// @name         Puzzmo Crossword Navigator
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Navigate to most recent incomplete crossword
// @author       Michael Abon
// @match        https://www.puzzmo.com/*
// @grant        none
// @esversion    8
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    function createButton(id = 'crossword-nav-btn') {
        const button = document.createElement('button');
        button.id = id;
        button.textContent = 'Go to Latest Incomplete Crossword';
        button.style.cssText = `
            padding: 0.25rem 1.5rem;
            width: 100%;
            background: var(--theme-player);
            color: var(--theme-playerFG);
            border: none;
            border-radius: 0.300rem;
            font-size: 13px;
            cursor: pointer;
            transition: background 0.2s;
            font-family: "Poppins-SemiBold", "Poppins SemiBold", "Poppins";
        `;
        
        button.addEventListener('click', navigateToLatestCrossword);
        return button;
    }
    
    function injectButton() {
        const todayIntro = document.getElementById('today-intro');
        if (!todayIntro || document.getElementById('crossword-nav-btn')) return;
        
        const button = createButton();
                
        // Insert before the target element
        const container = document.createElement('div');
        container.style.cssText = `
            margin-top: 1rem;
            display: flex;
            justify-content: flex-start;
        `;
        container.appendChild(button);
        todayIntro.parentNode.insertBefore(container, todayIntro);
    }
    
    function injectButtonInSidebar() {
        // Look for the specific link in the puzzle complete sidebar with "More puzzles" text
        const targetLinks = document.querySelectorAll('a[aria-label="Go to [object Object]"][href^="/today"]');
        const targetLink = Array.from(targetLinks).find(link => link.innerText.trim() === 'More puzzles');
        
        if (!targetLink || document.getElementById('crossword-nav-btn-sidebar')) return;
        
        const button = createButton('crossword-nav-btn-sidebar');
        
        // Create container for the button - this will be a sibling of the parent with onclick
        const container = document.createElement('div');
        container.style.cssText = `
            margin-bottom: 12px;
            display: flex;
            justify-content: flex-start;
        `;
        container.appendChild(button);
        
        // Get the parent with onclick and its parent (grandparent)
        const parentWithOnClick = targetLink.parentElement;
        const grandparent = parentWithOnClick.parentElement;
        
        if (grandparent) {
            // Insert our container before the parent with onclick
            grandparent.insertBefore(container, parentWithOnClick);
        }
    }
    
    function getAuthData() {
        try {
            const puzmoAuth = localStorage.getItem('puzmoAuth');
            if (!puzmoAuth) {
                throw new Error('No puzmoAuth found in localStorage');
            }
            
            const authData = JSON.parse(puzmoAuth);
            return {
                token: authData.token,
                userID: authData.data.userID
            };
        } catch (error) {
            console.error('Failed to parse auth data:', error);
            return null;
        }
    }
    
    async function getAuthHeaders() {
        const authData = getAuthData();
        if (!authData) {
            throw new Error('Could not retrieve authentication data');
        }
        
        return {
            'Accept': '*/*',
            'Accept-Language': 'en-CA,en-US;q=0.7,en;q=0.3',
            'auth-provider': 'custom',
            'authorization': `Bearer ${authData.token}`,
            'puzzmo-gameplay-id': authData.userID,
            'runtime': 'web',
            'Content-Type': 'application/json',
            'Origin': 'https://www.puzzmo.com',
            'Referer': window.location.href
        };
    }
    
    async function fetchCalendarData(month, year) {
        const query = `
            query CalendarQuery(
              $month: Int!
              $year: Int!
              $gameFilter: String
            ) {
              dailiesForAMonth(month: $month, year: $year, gameFilter: $gameFilter) {
                day
                puzzles(cacheKey: "calendar") {
                  series {
                    id
                  }
                  lock {
                    start
                    type
                    overrideRoles
                  }
                  status
                  urlPath
                  puzzle {
                    id
                    slug
                    name
                    emoji
                    variantID
                    subvariantID
                    seriesNumber
                    game {
                      slug
                      displayName
                      flagsArr
                      id
                    }
                    gameNameOverride
                    currentAccountGamePlayed {
                      slug
                      completed
                      pointsAwarded
                      id
                    }
                  }
                  id
                }
                id
              }
            }`;
        
        const headers = await getAuthHeaders();
        
        try {
            const response = await fetch('https://www.puzzmo.com/_api/prod/graphql?CalendarQuery=', {
                method: 'POST',
                headers,
                body: JSON.stringify({
                    query,
                    operationName: 'CalendarQuery',
                    variables: {
                        month,
                        year,
                        gameFilter: null
                    }
                })
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            console.error('Failed to fetch calendar data:', error);
            return null;
        }
    }
    
    function findIncompleteCrosswords(calendarData) {
        if (!calendarData?.data?.dailiesForAMonth) return [];
        
        const incompleteCrosswords = [];
        
        calendarData.data.dailiesForAMonth.forEach(daily => {
            if (!daily.puzzles) return;
            
            daily.puzzles.forEach(puzzleEntry => {
                const puzzle = puzzleEntry.puzzle;
                
                // Check if it's a crossword game
                if (puzzle?.game?.slug === 'crossword') {
                    const gamePlay = puzzle.currentAccountGamePlayed;
                    
                    // Check if incomplete (null gamePlay or completed: false)
                    if (!gamePlay || !gamePlay.completed) {
                        incompleteCrosswords.push({
                            day: daily.day,
                            urlPath: puzzleEntry.urlPath,
                            name: puzzle.name || puzzle.gameNameOverride || 'Crossword',
                            emoji: puzzle.emoji || '📝',
                            seriesNumber: puzzle.seriesNumber
                        });
                    }
                }
            });
        });
        
        // Sort by seriesNumber (most recent first - higher numbers are newer)
        return incompleteCrosswords.sort((a, b) => b.seriesNumber - a.seriesNumber);
    }
    
    async function navigateToLatestCrossword() {
        const button = document.getElementById('crossword-nav-btn') || document.getElementById('crossword-nav-btn-sidebar');
        const originalText = button.textContent;
        
        button.textContent = 'Searching...';
        button.disabled = true;
        
        try {
            const now = new Date();
            let currentMonth = now.getMonth(); // 0-indexed
            let currentYear = now.getFullYear();
            
            // Search back indefinitely (with reasonable safety limit of 5 years)
            const maxMonthsBack = 60; // 5 years
            
            for (let monthsBack = 0; monthsBack < maxMonthsBack; monthsBack++) {
                console.log(`Searching ${currentYear}-${currentMonth + 1}...`);
                button.textContent = `Searching ${currentYear}-${String(currentMonth + 1).padStart(2, '0')}...`;
                
                const calendarData = await fetchCalendarData(currentMonth, currentYear);
                if (!calendarData) {
                    console.error('Failed to fetch calendar data');
                    break;
                }
                
                const incompleteCrosswords = findIncompleteCrosswords(calendarData);
                
                if (incompleteCrosswords.length > 0) {
                    const latest = incompleteCrosswords[0];
                    console.log(`Found incomplete crossword: ${latest.name} on day ${latest.day}`);
                    button.textContent = `Found ${latest.name}! Navigating...`;
                    
                    // Navigate to the puzzle
                    window.location.href = `https://www.puzzmo.com/puzzle/${latest.urlPath}`;
                    return;
                }
                
                // Go back one month
                currentMonth--;
                if (currentMonth < 0) {
                    currentMonth = 11;
                    currentYear--;
                }
                
                // Small delay to avoid hammering the API
                await new Promise(resolve => setTimeout(resolve, 100));
            }
            
            alert(`No incomplete crosswords found in the last ${maxMonthsBack / 12} years!`);
            
        } catch (error) {
            console.error('Error finding crossword:', error);
            alert('Error finding incomplete crossword. Check console for details.');
        } finally {
            button.textContent = originalText;
            button.disabled = false;
        }
    }
    
    function injectAllButtons() {
        injectButton();
        injectButtonInSidebar();
    }
    
    // Wait for page to load, then inject
    function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', injectAllButtons);
        } else {
            injectAllButtons();
        }
        
        // Also watch for dynamic content changes
        const observer = new MutationObserver(injectAllButtons);
        observer.observe(document.body, { childList: true, subtree: true });
    }
    
    init();
})();