Backloggd - Steam Review Integration

Displays Steam reviews in unified format

// ==UserScript==
// @name         Backloggd - Steam Review Integration
// @namespace    https://greasyfork.org/en/users/1410951-nzar-bayezid
// @version      1.0
// @description  Displays Steam reviews in unified format
// @icon         https://www.backloggd.com/favicon.ico
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @match        https://backloggd.com/*
// @match        https://www.backloggd.com/*
// @grant        GM_xmlhttpRequest
// @connect      store.steampowered.com
// @license      MIT
// @downloadURL
// @updateURL
// ==/UserScript==

/*=========================  Version History  ==================================
v1.0 -
- Integrated Steam review data into Backloggd game pages
- Unified UI styling for consistent appearance with existing integrations
- Added support for displaying both recent and all-time Steam reviews
- Implemented dynamic color coding based on review sentiment (e.g., Positive, Negative)
- Optimized API request handling for Steam app ID and review data fetching
- Ensured proper error handling and fallbacks for missing or unavailable data
- Enhanced performance by minimizing redundant DOM manipulations
- Added MutationObserver to dynamically handle page updates and new content loading
- Improved localization of review counts using `toLocaleString` for better readability
*/

(function() {
    'use strict';
    const OBSERVER_CONFIG = {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
    };

    const REVIEW_COLORS = {
        'Overwhelmingly Positive': '#388E3C',
        'Very Positive': '#689F38',
        'Positive': '#AFB42B',
        'Mostly Positive': '#C0A128',
        'Mixed': '#FFA000',
        'Mostly Negative': '#F57C00',
        'Negative': '#E53935',
        'Very Negative': '#C62828',
        'Overwhelmingly Negative': '#B71C1C'
    };

    let processing = false;
    let currentPath = '';

    async function mainExecutor() {
        if (processing) return;
        if (location.pathname === currentPath) return;
        if (!document.querySelector('#game-body')) return;

        currentPath = location.pathname;
        processing = true;

        cleanExistingElements();
        await processGameData();
        processing = false;
    }

    function cleanExistingElements() {
        $('.steam-integration').remove();
    }

    async function processGameData() {
        try {
            const gameName = document.querySelector("#title h1").textContent;
            const appId = await fetchSteamAppId(gameName);
            if (!appId) return;

            const reviewData = await fetchSteamReviews(appId);
            renderSteamReview(reviewData, appId);
        } catch (error) {
            console.error('Steam Integration Error:', error);
        }
    }

    function fetchSteamAppId(gameName) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://store.steampowered.com/search/?term=${encodeURIComponent(gameName)}&category1=998`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
                },
                onload: function(response) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    const firstResult = doc.querySelector('.search_result_row');
                    resolve(firstResult?.dataset.dsAppid || null);
                },
                onerror: () => resolve(null),
                timeout: 5000
            });
        });
    }

    function fetchSteamReviews(appId) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://store.steampowered.com/app/${appId}`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
                    'Accept-Language': 'en-US,en;q=0.9'
                },
                onload: function(response) {
                    try {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const userReviews = doc.querySelector('#userReviews');

                        if (!userReviews) return resolve(null);

                        // Extract Recent Reviews
                        const recent = userReviews.querySelector('.user_reviews_summary_row:nth-child(1)');
                        const recentRating = recent?.querySelector('.game_review_summary')?.textContent;
                        const recentCount = extractNumber(recent?.querySelector('.responsive_hidden')?.textContent);

                        // Extract All Reviews
                        const all = userReviews.querySelector('.user_reviews_summary_row[itemprop="aggregateRating"]');
                        const allRating = all?.querySelector('.game_review_summary')?.textContent;
                        const allCount = extractNumber(all?.querySelector('.responsive_hidden')?.textContent);

                        resolve({
                            recent: { rating: recentRating, count: recentCount },
                            all: { rating: allRating, count: allCount }
                        });
                    } catch {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null),
                timeout: 5000
            });
        });
    }

    function extractNumber(text) {
        const match = text?.match(/\(([\d,]+)\)/);
        return match ? parseInt(match[1].replace(/,/g, '')) : null;
    }

    function formatReviewCount(num) {
        if (!num) return 'N/A';
        return num.toLocaleString();
    }

    function renderSteamReview(reviewData, appId) {
        if (!reviewData || !reviewData.all?.rating) return;

        const target = $("#game-body > div.col > div:nth-child(2) > div.col-12.col-lg-cus-32.mt-1.mt-lg-2");
        if (!target.length) return;

        const color = REVIEW_COLORS[reviewData.all.rating] || '#607D8B';

        const content = [];

        if (reviewData.recent?.rating) {
            content.push(`
                <div style="margin-bottom: 6px; font-size: 14px; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);">
                    <span style="opacity: 0.8;">Recent Reviews:<br>
                    ${reviewData.recent.rating}
                    (${formatReviewCount(reviewData.recent.count)})
                </div>
            `);
        }

        content.push(`
            <div style="font-size: 14px; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);">
                <span style="opacity: 0.8;">All Reviews:<br>
                ${reviewData.all.rating}
                (${formatReviewCount(reviewData.all.count)})
            </div>
        `);

        const element = $(`
            <div class="steam-integration" style="margin-top:10px;">
                <a href="https://store.steampowered.com/app/${appId}"
                   target="_blank"
                   style="display: block;
                          background: ${color};
                          color: white;
                          padding: 12px;
                          border-radius: 4px;
                          text-decoration: none;
                          transition: transform 0.2s;
                          border:1px solid #d1d1d1;
                          line-height: 1.4;">
                    ${content.join('')}
                </a>
            </div>
        `);

        target.append(element);
    }

    // Mutation observer setup
    new MutationObserver(mutations => {
        if (document.querySelector('#game-body') && mutations.some(m => m.addedNodes.length)) {
            mainExecutor();
        }
    }).observe(document.documentElement, OBSERVER_CONFIG);

    // Initial execution
    window.addEventListener('load', mainExecutor);
})();