Haiyaa Points!

Visualize a year of points availability with quickbook function on Hyatt points calendar

As of 2025-10-03. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Haiyaa Points!
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Visualize a year of points availability with quickbook function on Hyatt points calendar
// @author       World of Haiyaa
// @match        https://www.hyatt.com/explore-hotels/rate-calendar*
// @grant        GM_xmlhttpRequest
// @connect      www.hyatt.com
// ==/UserScript==

(function() {
    'use strict';

    function addGlobalStyle(css) {
        const head = document.head || document.getElementsByTagName('head')[0];
        if (!head) { return; }
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }

    addGlobalStyle(`
        .haiyaa-trigger-btn {
            background-color: #0D2D52; color: white; border: none; border-radius: 6px;
            padding: 6px 14px; font-size: 14px; font-weight: bold; cursor: pointer;
            margin-left: 20px; transition: all 0.2s;
        }
        .haiyaa-trigger-btn:hover { background-color: #1a4a87; }
        .haiyaa-trigger-btn:disabled { background-color: #888; cursor: not-allowed; }
        .visualizer-overlay {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background-color: rgba(0, 0, 0, 0.6); z-index: 9999; display: flex;
            justify-content: center; align-items: center;
        }
        .visualizer-modal {
            background-color: #ffffff; padding: 25px; border-radius: 12px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.3); width: auto; max-width: 95vw;
        }
        .modal-header {
            display: flex; justify-content: space-between; align-items: center;
            margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e0e0e0;
        }
        .modal-header h3 { margin: 0; font-size: 18px; }
        .modal-header select { margin-left: 20px; padding: 5px; border-radius: 5px; border: 1px solid #ccc; }
        .modal-close-btn { font-size: 24px; font-weight: bold; color: #888; cursor: pointer; border: none; background: none; }
        .modal-close-btn:hover { color: #000; }
        .calendar-chart-container {
            display: grid; grid-template-areas: "empty months" "weeks grid";
            grid-template-columns: auto 1fr; grid-template-rows: auto 1fr;
            gap: 5px 3px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
        }
        .calendar-months {
            grid-area: months; position: relative; height: 15px;
        }
        .calendar-months .month {
            position: absolute; top: 0; font-size: 11px; color: #555;
        }
        .calendar-weeks { grid-area: weeks; font-size: 11px; color: #555; }
        .points-calendar-grid { grid-area: grid; }
        .calendar-weeks .week-day { height: 14px; margin-bottom: 3px; display: flex; align-items: center; }
        .points-calendar-grid {
            display: grid; grid-template-columns: repeat(53, 14px); grid-template-rows: repeat(7, 14px);
            grid-auto-flow: column; grid-gap: 3px;
        }
        .calendar-day {
            width: 14px; height: 14px; background-color: transparent;
            border: 1px solid #e1e4e8; border-radius: 3px; transition: all 0.1s;
            position: relative;
        }
        .calendar-day.is-holiday {
            border-width: 2px;
            box-sizing: border-box;
        }
        .day-number {
            position: absolute; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            font-size: 9px; font-weight: bold; color: white;
            text-shadow: 0 0 2px rgba(0,0,0,0.7);
        }
        .calendar-day.clickable:hover {
            transform: scale(1.2); box-shadow: 0 0 5px rgba(0,0,0,0.5); cursor: pointer;
        }
        .calendar-day.unavailable {
            background-image: repeating-linear-gradient(45deg, rgba(255,255,255,0.7), rgba(255,255,255,0.7) 2px, transparent 2px, transparent 4px);
            cursor: not-allowed;
        }
        .calendar-day.off-peak { background-color: #216e39; border-color: transparent; }
        .calendar-day.standard { background-color: #9be9a8; border-color: transparent; }
        .calendar-day.peak { background-color: #ff9800; border-color: transparent; }
        .calendar-legend, .holiday-legend {
            display: flex; justify-content: center; align-items: center; flex-wrap: wrap;
            font-size: 12px; margin-top: 15px; color: #555;
        }
        .legend-item { display: flex; align-items: center; margin: 2px 8px; }
        .legend-color-box { width: 14px; height: 14px; border-radius: 3px; margin-right: 5px; }
        .holiday-legend .legend-color-box { border-width: 2px; border-style: solid; background-color: transparent; }
        .availability-disclaimer {
            font-size: 10px; font-style: italic;
            color: #888; text-align: center; margin-top: 15px;
            padding-top: 10px; border-top: 1px solid #eee;
        }
        .hidden { display: none !important; }
    `);

    function getHotelCodeFromURL() {
        return new URLSearchParams(window.location.search).get('spiritCode');
    }

    async function fetchPriceData(hotelCode) {
        const CHUNK_DAYS = 70;
        const promises = [];
        let currentDate = new Date();
        for (let i = 0; i < 6; i++) {
            const startDate = new Date(currentDate);
            const endDate = new Date(currentDate);
            endDate.setDate(endDate.getDate() + CHUNK_DAYS);
            const startDateStr = startDate.toISOString().split('T')[0];
            const endDateStr = endDate.toISOString().split('T')[0];
            const apiUrl = `https://www.hyatt.com/explore-hotels/service/avail?spiritCode=${hotelCode}&startDate=${startDateStr}&endDate=${endDateStr}`;
            promises.push(fetchSingleChunk(apiUrl));
            currentDate.setDate(currentDate.getDate() + CHUNK_DAYS + 1);
        }
        const settledResults = await Promise.all(promises);
        const successfulResults = settledResults.filter(Boolean);
        const allData = { STANDARD_ROOM: new Map(), CLUB: new Map(), STANDARD_SUITE: new Map(), PREMIUM_SUITE: new Map() };
        successfulResults.forEach(chunkData => {
            if (chunkData.STANDARD_ROOM) chunkData.STANDARD_ROOM.forEach((v, k) => allData.STANDARD_ROOM.set(k, v));
            if (chunkData.CLUB) chunkData.CLUB.forEach((v, k) => allData.CLUB.set(k, v));
            if (chunkData.STANDARD_SUITE) chunkData.STANDARD_SUITE.forEach((v, k) => allData.STANDARD_SUITE.set(k, v));
            if (chunkData.PREMIUM_SUITE) chunkData.PREMIUM_SUITE.forEach((v, k) => allData.PREMIUM_SUITE.set(k, v));
        });
        return allData;
    }

    function fetchSingleChunk(apiUrl) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET", url: apiUrl, headers: { "Accept": "application/json" },
                onload: function(response) {
                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        const roomCategories = data?.roomCategories;
                        const chunkData = {};
                        if (roomCategories?.STANDARD_ROOM) chunkData.STANDARD_ROOM = parseRoomData(roomCategories.STANDARD_ROOM);
                        if (roomCategories?.CLUB) chunkData.CLUB = parseRoomData(roomCategories.CLUB);
                        if (roomCategories?.STANDARD_SUITE) chunkData.STANDARD_SUITE = parseRoomData(roomCategories.STANDARD_SUITE);
                        if (roomCategories?.PREMIUM_SUITE) chunkData.PREMIUM_SUITE = parseRoomData(roomCategories.PREMIUM_SUITE);
                        resolve(chunkData);
                    } else { resolve(null); }
                },
                onerror: function() { resolve(null); }
            });
        });
    }

    function parseRoomData(roomData) {
        const priceMap = new Map();
        for (const date in roomData) {
            const details = roomData[date];
            const type = details.pointsLevel ? details.pointsLevel.toLowerCase() : 'unknown';
            priceMap.set(date, { type: type, points: details.pointsValue });
        }
        return priceMap;
    }

    async function fetchAvailabilityData(hotelCode) {
        const today = new Date();
        const endDate = new Date();
        endDate.setDate(today.getDate() + 365);
        const startDateStr = today.toISOString().split('T')[0];
        const endDateStr = endDate.toISOString().split('T')[0];
        const apiUrl = `https://www.hyatt.com/explore-hotels/service/avail/days?spiritCode=${hotelCode}&startDate=${startDateStr}&endDate=${endDateStr}&numAdults=1&numChildren=0&roomQuantity=1&los=1&isMock=false`;
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET", url: apiUrl, headers: { "Accept": "application/json" },
                onload: response => {
                    const availabilityByRoom = { STANDARD_ROOM: new Set(), CLUB: new Set(), STANDARD_SUITE: new Set(), PREMIUM_SUITE: new Set() };
                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        const days = data.days || {};
                        for (const date in days) {
                            const roomsOnDate = days[date];
                            if (roomsOnDate) {
                                if (roomsOnDate.STANDARD_ROOM) availabilityByRoom.STANDARD_ROOM.add(date);
                                if (roomsOnDate.CLUB) availabilityByRoom.CLUB.add(date);
                                if (roomsOnDate.STANDARD_SUITE) availabilityByRoom.STANDARD_SUITE.add(date);
                                if (roomsOnDate.PREMIUM_SUITE) availabilityByRoom.PREMIUM_SUITE.add(date);
                            }
                        }
                    }
                    resolve(availabilityByRoom);
                },
                onerror: () => resolve({ STANDARD_ROOM: new Set(), CLUB: new Set(), STANDARD_SUITE: new Set(), PREMIUM_SUITE: new Set() })
            });
        });
    }

    function getPointsTiers(priceMap) {
        const tiers = {};
        for (const item of priceMap.values()) {
            if (item.type && item.points && !tiers[item.type]) {
                tiers[item.type] = item.points;
            }
            if (tiers.off_peak && tiers.standard && tiers.peak) break;
        }
        return tiers;
    }

    function getHolidays(year) {
        const holidays = new Map();
        const nthWeekdayOfMonth = (n, weekday, month, year) => {
            let count = 0;
            const date = new Date(year, month, 1);
            while (count < n) {
                if (date.getDay() === weekday) count++;
                if (count < n) date.setDate(date.getDate() + 1);
            }
            return date;
        };
        const lastWeekdayOfMonth = (weekday, month, year) => {
            const date = new Date(year, month + 1, 0);
            while (date.getDay() !== weekday) {
                date.setDate(date.getDate() - 1);
            }
            return date;
        };
        const formatDate = (date) => date.toISOString().split('T')[0];

        holidays.set(formatDate(new Date(year, 0, 1)), { name: "New Year's Day", color: '#FFD700' });
        holidays.set(formatDate(nthWeekdayOfMonth(3, 1, 0, year)), { name: 'MLK Day', color: '#483D8B' });
        holidays.set(formatDate(lastWeekdayOfMonth(1, 4, year)), { name: 'Memorial Day', color: '#000080' });
        holidays.set(formatDate(new Date(year, 6, 4)), { name: 'Independence Day', color: '#B22222' });
        holidays.set(formatDate(nthWeekdayOfMonth(1, 1, 8, year)), { name: 'Labor Day', color: '#1E90FF' });
        holidays.set(formatDate(nthWeekdayOfMonth(4, 4, 10, year)), { name: 'Thanksgiving', color: '#A0522D' });
        holidays.set(formatDate(new Date(year, 11, 25)), { name: 'Christmas Day', color: '#228B22' });

        return holidays;
    }

    function createVisualizerUI(priceData, availabilityData) {
        const hotelCode = getHotelCodeFromURL();
        const currentYear = new Date().getFullYear();
        const holidays = new Map([...getHolidays(currentYear), ...getHolidays(currentYear + 1)]);
        const overlay = document.createElement('div');
        overlay.className = 'visualizer-overlay hidden';
        const modal = document.createElement('div');
        modal.className = 'visualizer-modal';
        const header = document.createElement('div');
        header.className = 'modal-header';
        header.innerHTML = `<h3>Points Calendar Quick View</h3>`;
        const selectorContainer = document.createElement('div');
        const selector = document.createElement('select');
        let optionsHTML = '';
        if (priceData.STANDARD_ROOM?.size > 0) optionsHTML += `<option value="STANDARD_ROOM">Standard Room</option>`;
        if (priceData.CLUB?.size > 0) optionsHTML += `<option value="CLUB">Club Access</option>`;
        if (priceData.STANDARD_SUITE?.size > 0) optionsHTML += `<option value="STANDARD_SUITE">Standard Suite</option>`;
        if (priceData.PREMIUM_SUITE?.size > 0) optionsHTML += `<option value="PREMIUM_SUITE">Premium Suite</option>`;
        selector.innerHTML = optionsHTML;
        if (optionsHTML) {
             selectorContainer.appendChild(selector);
             header.appendChild(selectorContainer);
        }
        const closeBtn = document.createElement('button');
        closeBtn.className = 'modal-close-btn';
        closeBtn.innerHTML = `&times;`;
        header.appendChild(closeBtn);
        const chartContainer = document.createElement('div');
        chartContainer.className = 'calendar-chart-container';
        const monthsContainer = document.createElement('div');
        monthsContainer.className = 'calendar-months';
        const weeksContainer = document.createElement('div');
        weeksContainer.className = 'calendar-weeks';
        const grid = document.createElement('div');
        grid.className = 'points-calendar-grid';
        chartContainer.append(monthsContainer, weeksContainer, grid);
        const legend = document.createElement('div');
        legend.className = 'calendar-legend';
        const holidayLegend = document.createElement('div');
        holidayLegend.className = 'holiday-legend';
        const disclaimer = document.createElement('div');
        disclaimer.className = 'availability-disclaimer';
        disclaimer.textContent = 'Availability based on one-night stay';
        modal.append(header, chartContainer, legend, holidayLegend, disclaimer);
        overlay.appendChild(modal);

        const weekDays = ['', 'Mon', '', 'Wed', '', 'Fri', ''];
        weekDays.forEach(day => { weeksContainer.innerHTML += `<div class="week-day">${day}</div>`; });

        let holidayLegendHTML = '';
        const sortedHolidays = Array.from(holidays.values()).sort((a,b) => a.name.localeCompare(b.name));
        const addedHolidays = new Set();
        for(const holiday of sortedHolidays){
            if(!addedHolidays.has(holiday.name)){
                holidayLegendHTML += `<div class="legend-item"><div class="legend-color-box" style="border-color: ${holiday.color};"></div> ${holiday.name}</div>`;
                addedHolidays.add(holiday.name);
            }
        }
        holidayLegend.innerHTML = holidayLegendHTML;

        function renderGrid(roomType) {
            grid.innerHTML = '';
            const priceMap = priceData[roomType];
            const availabilitySet = availabilityData[roomType] || new Set();
            if (!priceMap) return;
            let monthsHTML = '', lastMonth = -1, lastMonthLabelWeekIndex = -10;
            const startDate = new Date();
            startDate.setDate(startDate.getDate() - startDate.getDay());
            for (let i = 0; i < 371; i++) {
                const currentDate = new Date(startDate);
                currentDate.setDate(startDate.getDate() + i);
                const dayElement = document.createElement('div');
                dayElement.className = 'calendar-day';
                if (currentDate.getDay() === 0) {
                    const currentMonth = currentDate.getMonth();
                    if (currentMonth !== lastMonth) {
                        const weekIndex = Math.floor(i / 7);
                        if (weekIndex > lastMonthLabelWeekIndex + 1) {
                            const monthName = currentDate.toLocaleString('default', { month: 'short' });
                            const leftPosition = weekIndex * 17;
                            monthsHTML += `<div class="month" style="left: ${leftPosition}px;">${monthName}</div>`;
                            lastMonth = currentMonth;
                            lastMonthLabelWeekIndex = weekIndex;
                        }
                    }
                }
                if (currentDate >= new Date(new Date().setHours(0, 0, 0, 0))) {
                    const dateString = currentDate.toISOString().split('T')[0];
                    const dayData = priceMap.get(dateString);
                    let title = dateString;
                    const holiday = holidays.get(dateString);

                    if (currentDate.getDate() === 1) {
                        dayElement.innerHTML = `<div class="day-number">1</div>`;
                    }

                    if (dayData) {
                        dayElement.classList.add(dayData.type.replace('_', '-'));
                        title += `\n${dayData.points.toLocaleString()} Points (${dayData.type})`;
                        if(holiday){
                            dayElement.classList.add('is-holiday');
                            dayElement.style.borderColor = holiday.color;
                            title += `\n${holiday.name}`;
                        }
                        const isAvailable = availabilitySet.has(dateString);
                        if (isAvailable) {
                            dayElement.classList.add('clickable');
                            title += `\nClick to book!`;
                            dayElement.addEventListener('click', () => {
                                const checkoutDate = new Date(currentDate);
                                checkoutDate.setDate(checkoutDate.getDate() + 1);
                                const checkoutDateString = checkoutDate.toISOString().split('T')[0];
                                const bookingUrl = `https://www.hyatt.com/shop/rooms/${hotelCode}?checkinDate=${dateString}&checkoutDate=${checkoutDateString}&rooms=1&adults=1&kids=0&rateFilter=woh`;
                                window.open(bookingUrl, '_blank');
                            });
                        } else {
                            dayElement.classList.add('unavailable');
                            title += `\n(Not Available)`;
                        }
                    } else {
                        if(holiday){
                            dayElement.classList.add('is-holiday');
                            dayElement.style.borderColor = holiday.color;
                            title += `\n${holiday.name}`;
                        }
                        dayElement.classList.add('unavailable');
                        title += `\nNot priced`;
                    }
                    dayElement.title = title;
                }
                grid.appendChild(dayElement);
            }
            monthsContainer.innerHTML = monthsHTML;
            const pointTiers = getPointsTiers(priceMap);
            const legendOrder = [
                { key: 'off-peak', label: 'Off-Peak' }, { key: 'standard', label: 'Standard' }, { key: 'peak', label: 'Peak' }
            ];
            let legendHTML = '';
            for (const item of legendOrder) {
                const tierKey = item.key.replace('-', '_');
                const points = pointTiers[tierKey];
                if (points) {
                    legendHTML += `<div class="legend-item"><div class="legend-color-box calendar-day ${item.key}"></div> ${item.label} (${points.toLocaleString()})</div>`;
                }
            }
            legend.innerHTML = legendHTML;
        }

        selector.addEventListener('change', () => renderGrid(selector.value));
        closeBtn.addEventListener('click', () => overlay.classList.add('hidden'));
        overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.add('hidden'); });
        if(selector.value) renderGrid(selector.value);
        return overlay;
    }

    function waitForElement(selector) {
        return new Promise(resolve => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            const observer = new MutationObserver(() => {
                const targetEl = document.querySelector(selector);
                if (targetEl) { observer.disconnect(); resolve(targetEl); }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

    async function main() {
        const hotelCode = getHotelCodeFromURL();
        if (!hotelCode) return;
        const injectionSelector = 'body > div.explore-hotels-content > main > div.vrc-cal-container > div.vrc-calendar > div.calendar-body-header > div.calendar-date-container';
        const injectionPoint = await waitForElement(injectionSelector);
        if (!injectionPoint) {
            console.error('[Hyatt Visualizer] Could not find the injection point for the button.');
            return;
        }
        const triggerBtn = document.createElement('button');
        triggerBtn.className = 'haiyaa-trigger-btn';
        triggerBtn.textContent = 'Haiyaa!';
        injectionPoint.appendChild(triggerBtn);
        triggerBtn.addEventListener('click', async () => {
            const existingModal = document.querySelector('.visualizer-overlay');
            if (existingModal) existingModal.remove();
            triggerBtn.textContent = '...';
            triggerBtn.disabled = true;
            try {
                const [priceData, availabilityData] = await Promise.all([
                    fetchPriceData(hotelCode),
                    fetchAvailabilityData(hotelCode)
                ]);
                const visualizerModal = createVisualizerUI(priceData, availabilityData);
                document.body.appendChild(visualizerModal);
                visualizerModal.classList.remove('hidden');
            } catch (error) {
                console.error('[Hyatt Visualizer] A critical error occurred:', error);
                triggerBtn.textContent = 'Error!';
                setTimeout(() => { triggerBtn.textContent = 'Haiyaa!'; triggerBtn.disabled = false; }, 2000);
                return;
            }
            triggerBtn.textContent = 'Haiyaa!';
            triggerBtn.disabled = false;
        });
    }

    main();

})();