SSH XL Auto Responder & Urgent Room Opener

Automatically respond to SSH XL rooms and quickly open the 7 most urgent listings. Speeds up your student housing search!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         SSH XL Auto Responder & Urgent Room Opener
// @namespace    GreenMan36
// @version      1.0
// @description  Automatically respond to SSH XL rooms and quickly open the 7 most urgent listings. Speeds up your student housing search!
// @author       GreenMan36
// @match        https://www.sshxl.nl/nl/aanbod*
// @grant        none
// @license      MIT
// @homepage     https://github.com/GreenMan36
// @icon         https://www.sshxl.nl/favicon.ico
// ==/UserScript==

(function() {
    'use strict';

    console.log('SSH XL: Script starting...');
    console.log('SSH XL: Current URL is:', window.location.href);
    console.log('SSH XL: Current pathname is:', window.location.pathname);

    // AUTO-RESPONSE FUNCTIONALITY FOR INDIVIDUAL ROOM PAGES
    function isRoomPage() {
        const pathname = window.location.pathname;
        const aanbodMatch = pathname.match(/\/aanbod\/(.+)/);
        const hasRoomId = aanbodMatch && aanbodMatch[1] && aanbodMatch[1].trim() !== '';

        return hasRoomId;
    }

    // Wait for rooms to load on main page
    function waitForCards() {
        const cards = document.querySelectorAll('.card--property');

        if (cards.length > 0) {
            addUrgentOpenerButton();
        } else {
            setTimeout(waitForCards, 1000);
        }
    }

    // Add a floating button to trigger the urgent room opener
    function addUrgentOpenerButton() {
        // Remove existing button if present
        const existingButton = document.getElementById('ssh-urgent-opener');
        if (existingButton) existingButton.remove();

        const button = document.createElement('button');
        button.id = 'ssh-urgent-opener';
        button.innerHTML = '🏠 Open 7 Most Urgent';
        button.style.cssText = `
            position: fixed;
            bottom: 120px;
            right: 20px;
            background: #dc3545;
            color: white;
            border: none;
            padding: 15px 20px;
            border-radius: 25px;
            font-weight: bold;
            font-size: 14px;
            cursor: pointer;
            z-index: 9999;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            transition: all 0.3s ease;
        `;

        button.onmouseover = () => {
            button.style.background = '#c82333';
            button.style.transform = 'scale(1.05)';
        };
        button.onmouseout = () => {
            button.style.background = '#dc3545';
            button.style.transform = 'scale(1)';
        };

        button.onclick = openUrgentRoomsInteractive;

        document.body.appendChild(button);
    }

    function openUrgentRooms() {
        const cards = document.querySelectorAll('.card--property');

        if (cards.length === 0) {
            console.log('No property cards found on this page');
            return [];
        }

        console.log(`Found ${cards.length} total rooms`);

        // Take the last 7 cards (most urgent according to page ordering)
        const urgentCards = Array.from(cards).slice(-7);

        console.log(`Taking the last ${urgentCards.length} rooms (most urgent):`);

        // Extract data and open tabs
        const urgentRooms = urgentCards.map((card, index) => {
            const link = card.querySelector('.card__link');
            const timeElement = card.querySelector('.card__footer p');
            const title = card.querySelector('.card__title')?.textContent.trim() || 'Unknown';
            const price = card.querySelector('.price-tag')?.textContent.trim() || 'Unknown';

            if (!link || !timeElement) {
                console.log(`Card ${index}: Missing link or time element`);
                return null;
            }

            const href = link.getAttribute('href');
            const timeText = timeElement.textContent.trim();
            const fullUrl = window.location.origin + href;

            console.log(`${index + 1}. ${title} - ${price} - ${timeText}`);

            // Open in new tab with small delay
            setTimeout(() => {
                window.open(fullUrl, '_blank');
            }, index * 300);

            return {
                href: href,
                timeText: timeText,
                title: title,
                price: price
            };
        }).filter(room => room !== null);

        return urgentRooms;
    }

    function createRoomOverlay(urgentRooms) {
        // Remove existing overlay if present
        const existingOverlay = document.getElementById('urgent-rooms-overlay');
        if (existingOverlay) existingOverlay.remove();

        // Create overlay
        const overlay = document.createElement('div');
        overlay.id = 'urgent-rooms-overlay';
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.8);
            z-index: 10000;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: Arial, sans-serif;
        `;

        const content = document.createElement('div');
        content.style.cssText = `
            background: white;
            border-radius: 10px;
            padding: 20px;
            max-width: 600px;
            max-height: 80vh;
            overflow-y: auto;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        `;

        content.innerHTML = `
            <h2 style="margin-top: 0; color: #333;">🏠 7 Most Urgent Rooms</h2>
            <p style="color: #666; margin-bottom: 20px;">Click to open each room in a new tab:</p>
            <div id="room-buttons"></div>
            <button id="close-overlay" style="
                background: #dc3545;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                cursor: pointer;
                margin-top: 20px;
                float: right;
            ">Close</button>
            <div style="clear: both;"></div>
        `;

        const buttonContainer = content.querySelector('#room-buttons');

        // Add button for each room
        urgentRooms.forEach((room, index) => {
            const button = document.createElement('button');
            button.style.cssText = `
                display: block;
                width: 100%;
                text-align: left;
                background: #f8f9fa;
                border: 1px solid #dee2e6;
                padding: 15px;
                margin-bottom: 10px;
                border-radius: 5px;
                cursor: pointer;
                transition: background-color 0.2s;
            `;

            button.innerHTML = `
                <strong>${index + 1}. ${room.title}</strong><br>
                <span style="color: #28a745; font-weight: bold;">${room.price}</span><br>
                <span style="color: #dc3545; font-size: 0.9em;">${room.timeText}</span>
            `;

            button.onmouseover = () => button.style.backgroundColor = '#e9ecef';
            button.onmouseout = () => button.style.backgroundColor = '#f8f9fa';

            button.onclick = () => {
                const fullUrl = window.location.origin + room.href;
                window.open(fullUrl, '_blank');
                button.style.backgroundColor = '#d4edda';
                button.innerHTML += '<br><span style="color: #155724; font-size: 0.8em;">✓ Opened</span>';
            };

            buttonContainer.appendChild(button);
        });

        content.querySelector('#close-overlay').onclick = () => overlay.remove();
        overlay.onclick = (e) => {
            if (e.target === overlay) overlay.remove();
        };

        overlay.appendChild(content);
        document.body.appendChild(overlay);
    }

    function openUrgentRoomsInteractive() {
        const urgentRooms = openUrgentRooms();

        if (urgentRooms.length === 0) {
            alert('No rooms found! Make sure the page has loaded completely.');
            return;
        }

        console.log('\n🚨 If tabs didn\'t open automatically, use the overlay to click each room manually, this might be a bug.');

        createRoomOverlay(urgentRooms);

        const button = document.getElementById('ssh-urgent-opener');
        if (button) {
            const originalText = button.innerHTML;
            button.innerHTML = '✅ Opened!';
            button.style.background = '#28a745';
            setTimeout(() => {
                button.innerHTML = originalText;
                button.style.background = '#dc3545';
            }, 3000);
        }
    }

    function observePageChanges() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    const cards = document.querySelectorAll('.card--property');
                    if (cards.length > 0 && !document.getElementById('ssh-urgent-opener')) {
                        console.log('Changes detected, re-adding urgent room button.');
                        addUrgentOpenerButton();
                    }
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // for the room page
    function waitForReactButton() {
        return new Promise((resolve, reject) => {
            const maxWaitTime = 30000; // 30 seconds max wait
            const startTime = Date.now();

            function checkForButton() {
                console.log('SSH XL: Checking for "Reageer op deze kamer/woning" button...');

                // Try multiple selectors, idk which one worked in the end and am too lazy
                const selectors = [
                    '.pageheader__actions button',
                    'button[class*="button"]',
                    'button'
                ];

                let reactButton = null;
                let allButtons = [];

                for (const selector of selectors) {
                    const buttons = document.querySelectorAll(selector);
                    console.log(`SSH XL: Found ${buttons.length} buttons with selector "${selector}"`);

                    buttons.forEach((btn, index) => {
                        const text = btn.textContent || btn.innerText || '';
                        console.log(`SSH XL: Button ${index}: "${text.trim()}"`);
                        allButtons.push({selector, index, text: text.trim()});

                        if (text.includes('Reageer op deze kamer') || text.includes('Reageer op deze woning')) {
                            console.log('SSH XL: Found "Reageer op deze kamer/woning" button!');
                            reactButton = btn;
                        }
                    });
                }

                if (reactButton) {
                    resolve(reactButton);
                    return;
                }

                console.log('SSH XL: All buttons found:', allButtons);

                if (Date.now() - startTime > maxWaitTime) {
                    console.log('Timeout waiting for "Reageer op deze kamer/woning" button');
                    reject(new Error('Timeout waiting for react button'));
                    return;
                }

                setTimeout(checkForButton, 2000);
            }

            checkForButton();

          // might be redundant but idc, gotta refactor whenever but it works so who cares
            const observer = new MutationObserver((mutations) => {
                console.log('DOM mutation detected, checking for button...');
                const reactButton = document.querySelector('.pageheader__actions button');
                if (reactButton && (reactButton.textContent.includes('Reageer op deze kamer') || reactButton.textContent.includes('Reageer op deze woning'))) {
                    console.log('Found "Reageer op deze kamer/woning" button via observer!');
                    observer.disconnect();
                    resolve(reactButton);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            // Clean up observer on timeout
            setTimeout(() => {
                observer.disconnect();
            }, maxWaitTime);
        });
    }

    function waitForConfirmButton() {
        return new Promise((resolve, reject) => {
            const maxWaitTime = 10000;
            const startTime = Date.now();

            function checkForConfirmButton() {
                const confirmButton = document.querySelector('.modal__footer .button--primary');

                if (confirmButton && confirmButton.textContent.includes('Bevestigen')) {
                    resolve(confirmButton);
                    return;
                }

                if (Date.now() - startTime > maxWaitTime) {
                    reject(new Error('Timeout waiting for confirm button'));
                    return;
                }

                setTimeout(checkForConfirmButton, 200);
            }

            checkForConfirmButton();

          // might be redundant but idc, gotta refactor whenever but it works so who cares
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList') {
                        const confirmButton = document.querySelector('.modal__footer .button--primary');
                        if (confirmButton && confirmButton.textContent.includes('Bevestigen')) {
                            console.log('SSH XL: Found "Bevestigen" button via observer!');
                            observer.disconnect();
                            resolve(confirmButton);
                        }
                    }
                });
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
            }, maxWaitTime);
        });
    }

    async function clickReactButton(button) {
        addClickIndicator('Clicking "Reageer" button...', '#ff6b35');

        button.click();

        try {
            const confirmButton = await waitForConfirmButton();
            clickConfirmButton(confirmButton);
        } catch (error) {
            updateClickIndicator('❌ Error: Modal not found', '#dc3545');
        }
    }

    function clickConfirmButton(button) {
        // Update visual indicator
        updateClickIndicator('Clicking "Bevestigen"...', '#28a745');

        button.click();

        // Show success message
        setTimeout(() => {
            updateClickIndicator('✅ Response submitted!', '#28a745');
            setTimeout(() => {
                removeClickIndicator();
            }, 3000);
        }, 1000);
    }

    function addClickIndicator(message, color) {
        // Remove existing indicator
        removeClickIndicator();

        const indicator = document.createElement('div');
        indicator.id = 'ssh-click-indicator';
        indicator.innerHTML = message;
        indicator.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: ${color};
            color: white;
            padding: 15px 20px;
            border-radius: 10px;
            font-weight: bold;
            font-size: 14px;
            z-index: 10000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            animation: slideIn 0.3s ease;
        `;

        // Add animation
        const style = document.createElement('style');
        style.textContent = `
            @keyframes slideIn {
                from { transform: translateX(100%); opacity: 0; }
                to { transform: translateX(0); opacity: 1; }
            }
        `;
        document.head.appendChild(style);

        document.body.appendChild(indicator);
    }

    function updateClickIndicator(message, color) {
        const indicator = document.getElementById('ssh-click-indicator');
        if (indicator) {
            indicator.innerHTML = message;
            indicator.style.background = color;
        }
    }

    function removeClickIndicator() {
        const indicator = document.getElementById('ssh-click-indicator');
        if (indicator) indicator.remove();
    }

    // Initialize based on page type
    const roomPageCheck = isRoomPage();
    console.log('=== PAGE DETECTION COMPLETE ===');
    if (roomPageCheck) {
        console.log('❌ DETECTED ROOM PAGE - waiting for react button...');
        async function startAutoResponse() {
            try {
                addClickIndicator('Waiting for react button to load...', '#6c757d');
                const reactButton = await waitForReactButton();
                console.log('✅ REACT BUTTOn LOADED - applying for room now...');
                clickReactButton(reactButton);
            } catch (error) {
                updateClickIndicator('❌ Page load timeout', '#dc3545');
                setTimeout(removeClickIndicator, 5000);
            }
        }

        startAutoResponse();

    } else {
        console.log('❌ DETECTED LISTING PAGE - adding urgent opener...');

        // Initialize when page loads
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                console.log('✅ LISTING PAGE LOADED - waiting a bit before activating...');
                setTimeout(waitForCards, 2000); // Give extra time for content
                observePageChanges();
            });
        } else {
            console.log('✅ LISTING PAGE AlREADY LOADED - Activating...');
            setTimeout(waitForCards, 2000);
            observePageChanges();
        }
    }

})();