PreviewCart

предпросмотр корзины

// ==UserScript==
// @name         PreviewCart
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  предпросмотр корзины
// @author       HashBrute
// @match        https://lzt.market/*
// @grant        GM_addStyle
// @run-at       document-end
// @license      DDM
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
    .cart-hover-preview {
        position: absolute;
        width: 280px;
        max-width: 280px;
        background: #1f1f1f;
        border: 1px solid #363636;
        border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.5);
        z-index: 999999;
        overflow: hidden;
        display: none;
        pointer-events: auto !important;
        color: #ffffff;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
        box-sizing: border-box;
        max-height: 90vh;
    }

    .cart-hover-preview * {
        box-sizing: border-box;
    }

    .cart-hover-arrow {
        position: absolute;
        top: -8px;
        left: 50%;
        margin-left: -7px;
        width: 14px;
        height: 14px;
        background: #1f1f1f;
        transform: rotate(45deg);
        border-left: 1px solid #363636;
        border-top: 1px solid #363636;
        z-index: 1;
    }

    .cart-hover-content {
        padding: 12px;
        position: relative;
        z-index: 2;
        background: #1f1f1f;
        border-radius: 8px;
        max-height: calc(90vh - 24px);
        overflow-y: auto;
    }

    .cart-hover-header {
        text-align: center;
        display: flex;
        justify-content: center;
        align-items: center;
        margin-bottom: 12px;
        padding-bottom: 12px;
        border-bottom: 1px solid #363636;
    }

    .cart-hover-title {
        font-size: 15px;
        font-weight: 600;
        white-space: nowrap;
    }

    .cart-hover-count {
        background: #0daf77;
        color: white;
        border-radius: 12px;
        padding: 3px 9px;
        font-size: 12px;
        font-weight: 600;
        flex-shrink: 0;
        margin-left: 10px;
    }

    .cart-hover-items {
        max-height: 200px;
        overflow-y: auto;
        margin-bottom: 12px;
        scrollbar-width: thin;
        scrollbar-color: #363636 #1f1f1f;
    }

    .cart-hover-items::-webkit-scrollbar {
        width: 4px;
    }

    .cart-hover-items::-webkit-scrollbar-track {
        background: #1f1f1f;
    }

    .cart-hover-items::-webkit-scrollbar-thumb {
        background-color: #363636;
        border-radius: 3px;
    }

    .cart-hover-item {
        padding: 9px 0;
        border-bottom: 1px solid #363636;
        font-size: 13px;
    }

    .cart-hover-item:hover {
        background: rgba(255,255,255,0.03);
    }

    .cart-hover-item:last-child {
        border-bottom: none;
    }

    .cart-hover-item-title {
        color: #fff;
        margin-bottom: 4px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        line-height: 1.3;
    }

    .cart-hover-item-details {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 8px;
    }

    .cart-hover-item-seller {
        color: #949494;
        font-size: 12px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 65%;
    }

    .cart-hover-item-price {
        color: #0daf77;
        font-weight: 700;
        font-size: 12px;
        background: rgba(13, 175, 119, 0.15);
        padding: 4px 8px;
        border-radius: 4px;
        white-space: nowrap;
        flex-shrink: 0;
    }

    .cart-hover-empty {
        text-align: center;
        padding: 20px 0;
        color: #949494;
        font-size: 13px;
    }

    .cart-hover-footer {
        flex-direction: column;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-top: 12px;
        border-top: 1px solid #363636;
    }

    .cart-hover-total {
        font-size: 15px;
        font-weight: 600;
        display: flex;
        align-items: center;
        width: 100%;
        justify-content: space-between;
        margin-bottom: 12px;
    }

    .cart-hover-total-price {
        background: #0daf77;
        color: white;
        padding: 5px 12px;
        border-radius: 5px;
        font-size: 14px;
    }

    .cart-hover-buttons {
        display: flex;
        gap: 10px;
        width: 100%;
    }

    .cart-hover-button {
        text-align: center;
        background: #0daf77;
        color: white !important;
        border-radius: 5px;
        font-weight: 600;
        text-decoration: none !important;
        transition: background 0.2s;
        font-size: 13px;
        border: none;
        cursor: pointer;
        padding: 9px 15px;
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .cart-hover-button-cart {
        padding: 9px;
        flex: 0 0 40px;
    }

    .cart-hover-button:hover {
        background: #09965e;
    }

    .cart-loader {
        text-align: center;
        padding: 20px;
    }

    .cart-loader-spinner {
        display: inline-block;
        width: 20px;
        height: 20px;
        border: 2px solid rgba(13, 175, 119, 0.3);
        border-radius: 50%;
        border-top-color: #0daf77;
        animation: cart-spin 1s linear infinite;
    }

    @keyframes cart-spin {
        to { transform: rotate(360deg); }
    }

    @media (max-width: 768px) {
        .cart-hover-preview {
            width: calc(100vw - 20px);
            max-width: 320px;
        }

        .cart-hover-item-price {
            font-size: 11px;
            padding: 3px 6px;
        }

        .cart-hover-button {
            font-size: 12px;
            padding: 8px 12px;
        }
    }
`);


    let cartPreviewEl = null;
    let cartData = null;
    let lastFetchTime = 0;
    let previewTimeout = null;
    const FETCH_COOLDOWN = 5000; 


    let isInitialized = false;

    const DEBUG_MODE = false; 


    function debugLog(...args) {
        if (DEBUG_MODE) {
            console.log('[LZT Cart Preview]', ...args);
        }
    }

    function errorLog(...args) {
        console.error('[LZT Cart Preview]', ...args);
    }


    function findCartElement() {
        try {
            const selectors = [
                '.navCart.navLink',
                '.navTab.PopupClosed a[href*="cart"]',
                '.navTab a[href*="cart"]',
                'a[href*="cart"]',
                'a[href*="mass-buy/cart"]',
                '.link--internal[href*="cart"]',
                '.p-navgroup-link[href*="cart"]'
            ];

            for (const selector of selectors) {
                const element = document.querySelector(selector);
                if (element) {
                    return element;
                }
            }

            const iconSelectors = [
                'svg.feather-shopping-cart',
                '.fa-shopping-cart',
                'i.fa-cart'
            ];

            for (const selector of iconSelectors) {
                const icon = document.querySelector(selector);
                if (icon) {
                    const parent = icon.closest('a') || icon.closest('button') || icon.closest('.navTab') || icon.parentElement;
                    if (parent) {
                        return parent;
                    }
                    return icon;
                }
            }

            return null;
        } catch (error) {
            return null;
        }
    }


    function initCartPreview() {
        if (isInitialized) {
            return;
        }

        const cartElement = findCartElement();
        if (!cartElement) {
            setTimeout(initCartPreview, 2000);
            return;
        }

        cartPreviewEl = document.createElement('div');
        cartPreviewEl.className = 'cart-hover-preview';
        cartPreviewEl.innerHTML = '<div class="cart-hover-arrow"></div><div class="cart-hover-content"><div class="cart-loader"><div class="cart-loader-spinner"></div></div></div>';

        document.body.appendChild(cartPreviewEl);

        function updatePosition() {
            try {
                const rect = cartElement.getBoundingClientRect();
                if (!rect || rect.width === 0) {
                    return;
                }

                const scrollTop = window.scrollY || document.documentElement.scrollTop;

                cartPreviewEl.style.position = 'absolute';
                cartPreviewEl.style.zIndex = '999999';

                const windowWidth = window.innerWidth;
                const previewWidth = 280;

                let leftPos = rect.left + (rect.width / 2) - (previewWidth / 2);

                if (leftPos < 10) leftPos = 10;
                if (leftPos + previewWidth > windowWidth - 10) {
                    leftPos = windowWidth - previewWidth - 10;
                }

                cartPreviewEl.style.top = (rect.bottom + scrollTop + 5) + 'px';
                cartPreviewEl.style.left = leftPos + 'px';

                const arrowEl = cartPreviewEl.querySelector('.cart-hover-arrow');
                if (arrowEl) {
                    const arrowLeftPos = (rect.left + (rect.width / 2) - leftPos);

                    const minArrowPos = 15; 
                    const maxArrowPos = previewWidth - 15 - 14; 

                    let finalArrowPos = Math.max(minArrowPos, Math.min(arrowLeftPos, maxArrowPos));
                    arrowEl.style.left = finalArrowPos + 'px';
                    arrowEl.style.right = 'auto';
                    arrowEl.style.marginLeft = '0';
                }

                debugLog('Обновлена позиция окна:', {
                    top: cartPreviewEl.style.top,
                    left: cartPreviewEl.style.left
                });
            } catch (error) {
                errorLog('Ошибка при обновлении позиции:', error);
            }
        }

        window.addEventListener('resize', updatePosition);
        document.addEventListener('scroll', updatePosition, { passive: true });

        const enterHandler = function(e) {
            clearTimeout(previewTimeout);
            updatePosition(); 
            handleCartHover();
        };

        const leaveHandler = function(e) {
            previewTimeout = setTimeout(() => {
                if (cartPreviewEl) {
                    cartPreviewEl.style.display = 'none';
                }
            }, 300);
        };


        cartElement.addEventListener('mouseenter', enterHandler);
        cartElement.addEventListener('mouseleave', leaveHandler);


        cartElement.addEventListener('touchstart', enterHandler, { passive: true });


        cartPreviewEl.addEventListener('mouseenter', function(e) {
            clearTimeout(previewTimeout);
        });

        cartPreviewEl.addEventListener('mouseleave', function(e) {
            if (cartPreviewEl) {
                cartPreviewEl.style.display = 'none';
            }
        });

        window.showCartPreview = function() {
            if (cartPreviewEl && cartElement) {
                updatePosition();
                handleCartHover();
                return;
            } else {
                return;
            }
        };

        isInitialized = true;
    }

    async function handleCartHover() {
        if (!cartPreviewEl) {
            return;
        }

        cartPreviewEl.style.display = 'block';
        cartPreviewEl.style.visibility = 'visible';

        const now = Date.now();
        if (!cartData || now - lastFetchTime > FETCH_COOLDOWN) {
            try {
                cartPreviewEl.querySelector('.cart-hover-content').innerHTML = '<div class="cart-loader"><div class="cart-loader-spinner"></div></div>';

                debugLog('Запрос данных корзины...');
                const response = await fetch('https://lzt.market/user/cart', {
                    credentials: 'include',
                    cache: 'no-cache' 
                });

                if (!response.ok) {
                    throw new Error(`Ошибка HTTP: ${response.status}`);
                }

                const html = await response.text();
                cartData = parseCartData(html);
                lastFetchTime = Date.now();
                renderCartPreview();
            } catch (error) {
                if (cartPreviewEl && cartPreviewEl.querySelector('.cart-hover-content')) {
                    cartPreviewEl.querySelector('.cart-hover-content').innerHTML = '<div class="cart-hover-empty">Ошибка загрузки</div>';
                }
            }
        } else {
            debugLog('Используем закешированные данные корзины');
            renderCartPreview();
        }
    }

    function parseCartData(html) {
        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const items = [];
            const itemElements = doc.querySelectorAll('.marketIndexItem');


            itemElements.forEach((item, index) => {
                try {
                    const titleEl = item.querySelector('.marketIndexItem--Title');
                    const priceEl = item.querySelector('.marketIndexItem--Price .Value');
                    const sellerEl = item.querySelector('.username');

                    if (titleEl && priceEl) {
                        items.push({
                            title: titleEl.textContent.trim(),
                            price: priceEl.textContent.trim(),
                            seller: sellerEl ? sellerEl.textContent.trim() : 'Продавец'
                        });
                    }
                } catch (e) {
                    errorLog('Ошибка при парсинге товара:', e);
                }
            });

            if (items.length === 0) {
                try {
                    const alternativeItems = doc.querySelectorAll('.structItem--product, .cart-item, [class*="cart-item"]');
                    debugLog('Альтернативный поиск товаров:', alternativeItems.length);

                    alternativeItems.forEach((item, index) => {
                        try {
                            const titleEl = item.querySelector('[class*="title"], .item-title, h3, h4, .structItem-title');
                            const priceEl = item.querySelector('[class*="price"], .product-price, .price, .cost');
                            const sellerEl = item.querySelector('.username, .seller, [class*="seller"]');

                            if (titleEl && priceEl) {
                                items.push({
                                    title: titleEl.textContent.trim(),
                                    price: priceEl.textContent.trim().replace(/[^\d.,]/g, ''),
                                    seller: sellerEl ? sellerEl.textContent.trim() : 'Продавец'
                                });
                            }
                        } catch (e) {
                            errorLog('Ошибка при парсинге альтернативного товара:', e);
                        }
                    });
                } catch (e) {
                    errorLog('Ошибка при альтернативном парсинге товаров:', e);
                }
            }

            let totalCount = '0';
            try {
                const countSelectors = [
                    '.marketCart-subtitle span',
                    '.cart-count',
                    '[class*="cart-count"]',
                    '.badge',
                    '.counter'
                ];

                for (const selector of countSelectors) {
                    const countEl = doc.querySelector(selector);
                    if (countEl) {
                        totalCount = countEl.textContent.trim();
                        break;
                    }
                }

                if (totalCount === '0' && items.length > 0) {
                    totalCount = items.length.toString();
                }
            } catch (e) {
                errorLog('Ошибка при парсинге количества:', e);
                if (items.length > 0) {
                    totalCount = items.length.toString();
                }
            }

            let totalPrice = '0 ₽';
            try {
                const priceSelectors = [
                    '.marketCart-title-green',
                    '.cart-total',
                    '[class*="cart-total"]',
                    '.total-price',
                    '.price.total'
                ];

                for (const selector of priceSelectors) {
                    const priceEl = doc.querySelector(selector);
                    if (priceEl) {
                        totalPrice = priceEl.textContent.trim();
                        break;
                    }
                }

                if ((totalPrice === '0 ₽' || totalPrice === '0₽') && items.length > 0) {
                    let sum = 0;
                    items.forEach(item => {
                        const price = parseFloat(item.price.replace(/[^\d.,]/g, '').replace(',', '.'));
                        if (!isNaN(price)) {
                            sum += price;
                        }
                    });

                    if (sum > 0) {
                        totalPrice = sum + ' ₽';
                    }
                }

                if (!totalPrice.includes('₽')) {
                    totalPrice += ' ₽';
                }

                if (totalPrice.includes('₽') && !totalPrice.includes(' ₽')) {
                    totalPrice = totalPrice.replace('₽', ' ₽');
                }
            } catch (e) {
                errorLog('Ошибка при парсинге цены:', e);
            }

            return {
                items: items,
                count: totalCount,
                totalPrice: totalPrice
            };
        } catch (error) {
            errorLog('Ошибка при парсинге данных корзины:', error);
            return {
                items: [],
                count: '0',
                totalPrice: '0 ₽'
            };
        }
    }

    function renderCartPreview() {
        if (!cartPreviewEl || !cartData) {
            errorLog('Невозможно отрендерить предпросмотр: отсутствуют необходимые данные');
            return;
        }

        const contentEl = cartPreviewEl.querySelector('.cart-hover-content');
        if (!contentEl) {
            errorLog('Не найден элемент содержимого');
            return;
        }

        if (!cartData.items || cartData.items.length === 0) {
            contentEl.innerHTML = '<div class="cart-hover-empty">Ваша корзина пуста</div>';
            return;
        }

        let html = `
            <div class="cart-hover-header">
                <div class="cart-hover-title">Корзина</div>
                <div class="cart-hover-count">${cartData.count}</div>
            </div>
            <div class="cart-hover-items">
        `;

        cartData.items.forEach(item => {
            let price = item.price || '0';

            if (!price.includes('₽')) {
                price += ' ₽';
            }

            if (price.includes('₽') && !price.includes(' ₽')) {
                price = price.replace('₽', ' ₽');
            }

            html += `
                <div class="cart-hover-item">
                    <div class="cart-hover-item-title">${item.title || 'Товар'}</div>
                    <div class="cart-hover-item-details">
                        <span class="cart-hover-item-seller">${item.seller || 'Продавец'}</span>
                        <span class="cart-hover-item-price">${price}</span>
                    </div>
                </div>
            `;
        });

        let formattedTotal = cartData.totalPrice || '0 ₽';

        if (!formattedTotal.includes('₽')) {
            formattedTotal += ' ₽';
        }

        if (formattedTotal.includes('₽') && !formattedTotal.includes(' ₽')) {
            formattedTotal = formattedTotal.replace('₽', ' ₽');
        }

        html += `
            </div>
            <div class="cart-hover-footer">
                <div class="cart-hover-total">Итого: <span class="cart-hover-total-price">${formattedTotal}</span></div>
            </div>
            <div class="cart-hover-buttons">
                <a href="https://lzt.market/mass-buy/cart" class="cart-hover-button cart-hover-button-mass">Массовая покупка</a>
                <a href="https://lzt.market/user/cart" class="cart-hover-button cart-hover-button-cart">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="9" cy="21" r="1"></circle>
                        <circle cx="20" cy="21" r="1"></circle>
                        <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                    </svg>
                </a>
            </div>
        `;

        contentEl.innerHTML = html;
        debugLog('Предпросмотр корзины отрендерен');

        cartPreviewEl.style.display = 'block';
        cartPreviewEl.style.visibility = 'visible';
    }


    function checkPageReady() {
        try {
            if (document.body) {
                const cartElement = findCartElement();

                if (cartElement) {
                    debugLog('Страница загружена, корзина найдена, запускаем скрипт');
                    initScript();
                } else {
                    debugLog('Страница загружена, но корзина не найдена. Ожидаем...');
                    setTimeout(checkPageReady, 1000);
                }
            } else {
                debugLog('Страница еще не полностью загружена, ожидаем...');
                setTimeout(checkPageReady, 500);
            }
        } catch (error) {
            errorLog('Ошибка при проверке загрузки страницы:', error);
            setTimeout(checkPageReady, 1000);
        }
    }

    function initScript() {
        try {
            debugLog('Запуск скрипта предпросмотра корзины...');
            // Проверяем, есть ли уже элемент предпросмотра
            if (document.querySelector('.cart-hover-preview') || isInitialized) {
                debugLog('Элемент предпросмотра уже существует');
                return;
            }

            initCartPreview();
        } catch (error) {
            errorLog('Ошибка при инициализации скрипта:', error);
        }
    }

    setTimeout(() => {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => setTimeout(checkPageReady, 500));
        } else {
            checkPageReady();
        }
    }, 500);

    const observer = new MutationObserver((mutations) => {
        if (!isInitialized) {
            const cartElement = findCartElement();
            if (cartElement) {
                debugLog('DOM изменился, корзина найдена, инициализируем предпросмотр');
                initScript();
            }
        }
    });

    try {
        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: true });
            debugLog('MutationObserver запущен');
        } else {
            document.addEventListener('DOMContentLoaded', () => {
                observer.observe(document.body, { childList: true, subtree: true });
                debugLog('MutationObserver запущен после загрузки DOM');
            });
        }
    } catch (error) {
        errorLog('Ошибка при установке MutationObserver:', error);
        setInterval(() => {
            if (!isInitialized) {
                checkPageReady();
            }
        }, 3000);
    }

})();