atbmarket.com - price for kilogram

Shows price per kilo for products in catalog/search/product page. Improves voice search expirience with dictanote.co chrome extension

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         atbmarket.com - price for kilogram
// @name:uk      atbmarket.com - ціна за кілограм
// @namespace    http://tampermonkey.net/
// @version      1.15
// @description  Shows price per kilo for products in catalog/search/product page. Improves voice search expirience with dictanote.co chrome extension
// @description:uk Показує ціну за кілограм для продуктів у каталозі/пошуку/на сторінці продукту. Покращує роботу голосового пошуку за допомогою розширення dictanote.co для Google Chrome
// @author       Untiy16
// @license      MIT
// @match        https://www.atbmarket.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=atbmarket.com
// @grant        GM_addStyle
// @require      https://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==

'use strict';

//regex to parse weight from product title
const regex = /(\d+(?:[\.,]\d+)?)\s*(г|кг|мл|л)(?=($|\s|[^a-zA-Z0-9_а-яА-я]))/m;
const regexTeaBag = /(\d+)\s*(ф\/п)\s*(\*|x|X|х|Х)\s*(\d+(?:[\.,]\d+)?)/m;
let isActive = (localStorage.getItem('pricePerKiloMode') ?? 'true') === 'true';

//Add toggle to quick disable/enable 'price for kilo' functionality
$('.top-header__phone-tablet')
    .clone()
    .insertAfter($('.top-header__phone-tablet'))
    .html(`Ціна за кілограм <input type="checkbox" id="pricePerKiloMode" ${isActive ? 'checked' : ''}>`);

//add class to body and state to localStorage
$(pricePerKiloMode)
    .change(function() {
        isActive = pricePerKiloMode.checked;
        localStorage.setItem('pricePerKiloMode', isActive);
        $('body').toggleClass('price-per-kilo_global--hidden', !isActive);
    })
    .change();

//-------------- TRIGGER VOICE INPUT SECTION START ---------------//
setInterval(() => {
    if ($('#q').is(':visible') && $('#voicein_container').is(':visible')) {
        //trigger site search after voice input
        if (q.value != '' && Multisearch.config.hash != encodeURI(q.value)) {
            Multisearch.config.hash = encodeURI(q.value);
            location.href = `${location.pathname}${Multisearch.config.search_path}${q.value}`;
            // location.reload();
        }

        //erace previous text from search input before inserting
        if ($('#q').is(':visible') && $('#voicein_container').length && $($('#voicein_container')[0].shadowRoot).find('#voicein_voicebox').is(':visible')) {
            q.value = '';
        }
    }
}, 1000);
//-------------- TRIGGER VOICE INPUT SECTION END ---------------//

//-------------- LIST SECTION START ---------------//
//add price per kilo blocks to products in catalog
function handleCatalogItem(item) {
    let title = $(item).find('.catalog-item__title').text().trim();
    let match = getMatch(title);
    if (match !== null) {
        $(item)
            .find('> .catalog-item__bottom')
            .clone()
            .insertAfter($(item).find('> .catalog-item__bottom'))
            .addClass('price_per_kilo catalog-item__price-per-kilo catalog-item__price-per-kilo--value catalog-item__price-per-kilo--hidden')
            .find('.catalog-item__counter').remove();

        let $priceBlock = $(item).find('> .catalog-item__bottom').last();
        insertPricesPerKilo($priceBlock, getFormatedWeight(match));
    }
}

$(document).on('mouseenter mouseleave', 'article.catalog-item', function(e) {
    if (!$(this).find('.catalog-item__price-per-kilo').length) {
        handleCatalogItem(this);
    }

    $(this).find('.catalog-item__price-per-kilo').toggleClass('catalog-item__price-per-kilo--hidden', e.type === 'mouseleave');

    insertUnitWeight($(this));
});

// (new URLSearchParams(window.location.search)).get('page');
$('.catalog-page__sort').prepend('<button class="sort_by_price_pe_kilo">Сортувати за ціною за кілограм</button>');
$('.sort_by_price_pe_kilo').on('click', function () {
    let runSort = function () {
        $('.sort_by_price_pe_kilo').text('Готово!').prop('disabled', 1);
        sortCatalogByPricePerKilo();
    };
    let currentPage = parseInt((new URLSearchParams(window.location.search)).get('page'));
    if (!isNaN(currentPage) && currentPage > 1) {
        location.href = `${location.href.replace(/&page=\d+/gm, '').replace(/\page=\d+&/gm, '').replace(/\page=\d+/gm, '')}${location.href.includes('?') ? '&' : '?'}runSortByPricePerKilo=1`;
        return false;
    }
    let havePagination = $('.product-pagination .product-pagination__item').length;
    if (havePagination) {
        $('.main-container').addClass('main-container--loading');
        loadPaginatedProductsRecursively(
            $('.product-pagination__item.active + .product-pagination__item a').attr('href'),
            () => {
                $('.main-container').removeClass('main-container--loading');
                $('.product-pagination').hide();
                runSort();
            },
            () => {
                $('.main-container').removeClass('main-container--loading');
            }
        );
    } else {
        runSort();
    }
});

if (location.href.includes('runSortByPricePerKilo=1')) {
    $('.sort_by_price_pe_kilo').click();
}


//-------------- LIST SECTION END ---------------//

//--------------PROD PAGE SECTION START ---------------//
//add price per kilo block on product page
if (location.pathname.includes('/product/')) {
    insertUnitWeight($('.product-about__buy-row'));
    let title = $('.product-page__title').text().trim();
    let match = getMatch(title);
    if (match !== null) {
        $('.product-about__buy-row').clone().insertAfter($('.product-about__buy-row'));
        let $priceBlock = $('.product-about__buy-row').last();
        $priceBlock.css({'grid-row': 'unset'});
        $priceBlock.addClass('price_per_kilo product-about__price-per-kilo product-about__price-per-kilo--value');
        $priceBlock.find('.product-about__counter').remove();
        insertPricesPerKilo($priceBlock, getFormatedWeight(match));
    }
}
//--------------PROD PAGE SECTION END ---------------//

//--------------SEARCH SECTION START ---------------//
//add price per kilo and relevant price to products in search popap
$(document).on('mouseenter', '.multi-search .multi-item', function() {
    let $this = $(this);
    if ($this.find('.search-custom-price').length || $this.hasClass('search-custom-price--loading')) {
        return false;
    }

    $this.addClass('search-custom-price--loading');
    let title = $this.find('.multi-content a span').text().trim();
    let match = getMatch(title);

    let url = $this.find('.multi-content a').attr('href');
    $.get(url, function(data) {
        let $data = $(data).find('.product-main .product-price--weight');
        let price = parseFloat($data.find('.product-price__top span').text());
        let oldPrice = parseFloat($data.find('.product-price__bottom span').text());
        let card = parseFloat($data.find('.atbcard-sale__price-top span').text());
        $this.find('.multi-oldPrice, .multi-price').hide();
        $(getSearchPricesHtml(match ? getFormatedWeight(match) : 0, price, oldPrice, card)).insertBefore($this.find('.b-addToCart'));
    }).always(function() {
        $this.removeClass('search-custom-price--loading');
    });
});
//--------------SEARCH SECTION END ---------------//


//-------------- HELPERS ---------------//
function getSearchPricesHtml(weight, price, oldPrice = 0, card = 0) {
    let picePerK
    return `
        <div class="search-custom-price">
            ${oldPrice ? `<div class="search-custom-price_bottom">${addTracingZero(oldPrice)}</div>` : ''}
            <div class="search-custom-price_top">${addTracingZero(price)}</div>
            ${card ? `<div class="search-custom-price_card">${addTracingZero(card)}</div>` : ''}
        </div>
        ${weight ?
            `
            <div class="price_per_kilo search-custom-price__price-per-kilo search-custom-price__price-per-kilo--text">-- Ціна за кілограм --</div>
            <div class="search-custom-price price_per_kilo search-custom-price__price-per-kilo .search-custom-price__price-per-kilo--text">
                ${oldPrice ? `<div class="search-custom-price_bottom">${getPricePerKilo(oldPrice, weight).price}</div>` : ''}
                <div class="search-custom-price_top">${getPricePerKilo(price, weight).price}</div>
                ${card ? `<div class="search-custom-price_card">${getPricePerKilo(card, weight).price}</div>` : ''}
            </div>`
            :
            ''
        }
    `;
}

function getPricePerKilo(price, weight) {
    price = Math.round((price * 1000 / weight) * 100) / 100;
    price = price.toString();
    let intPart = price.split('.')[0];
    let decimalPart = price.split('.')[1];
    decimalPart = decimalPart ? decimalPart : '0';
    decimalPart = addTracingZero(decimalPart, true);

    return {
        'price': addTracingZero(price),
        'int': intPart,
        'decimal': decimalPart,
    }

}

function addTracingZero(price, isDecimalPart = false) {
    price = price.toString();
    // price = !price.includes('.') && !isDecimalPart ? `${price}.00` : price;

    return (
            (isDecimalPart && price.length === 2) ||
            (price.includes('.') && price.split('.')[1].length === 2) ||
            (!price.includes('.') && !isDecimalPart)
        )
        ?
        price
        :
        `${price}0`;
}

function getFormatedWeight(match) {
    match[1] = match[1].replaceAll(',', '.');
    return (match[2] === 'кг'
    || match[2] === 'л')
        ? match[1] * 1000 : parseFloat(match[1]);
}

function insertPricesPerKilo($priceBlock, weight) {
    let hasUnitSwitch = $priceBlock.find('.change-weight').length > 0;
    if (hasUnitSwitch) {
        return;
    }
    let $cardPrice = $priceBlock.find('.atbcard-sale__price-top');
    if ($cardPrice.length) {
        let price = getPricePerKilo($cardPrice.attr('value'), weight);
        $cardPrice.html(`<span>${price.int}.<sup class="product-price__coin">${price.decimal}</sup></span>`);
    }

    let $bottomPrice = $priceBlock.find('.product-price__bottom');
    if ($bottomPrice.length) {
        let price = getPricePerKilo(parseFloat($bottomPrice[0].innerText), weight);
        $bottomPrice = $bottomPrice.find('> span').eq(0);
        $bottomPrice.html(`<span>${price.int}.<sup class="product-price__coin">${price.decimal}</sup></span>`);
    }

    let $topPrice = $priceBlock.find('.product-price__top');
    if ($topPrice.length) {
        let price = getPricePerKilo(parseFloat($topPrice[0].innerText), weight);
        $topPrice = $topPrice.find('> span').eq(0);
        $topPrice.html(`<span>${price.int}.<sup class="product-price__coin">${price.decimal}</sup></span>`);
        $topPrice.next().find('span').text('/кг');
    }

}

function getLowestPricePerKilo($priceBlock, weight) {
    let prefixSelector =  '.product-price--weight';
    if (weight === null) {
        weight = 1000;
    }

    let pricesPerKilo = [];
    let $topPrice = $priceBlock.find(`${prefixSelector} .product-price__top`);
    if ($topPrice.length) {
        pricesPerKilo.push(getPricePerKilo(parseFloat($topPrice[0].innerText), weight).price);
    }

    let $bottomPrice = $priceBlock.find(`${prefixSelector} .product-price__bottom`);
    if ($bottomPrice.length) {
        pricesPerKilo.push(getPricePerKilo(parseFloat($bottomPrice[0].innerText), weight).price);
    }

    let $cardPrice = $priceBlock.find(`${prefixSelector} .atbcard-sale__price-top`);
    if ($cardPrice.length) {
        pricesPerKilo.push(getPricePerKilo($cardPrice.attr('value'), weight).price);
    }

    return pricesPerKilo.length ? Math.min(...pricesPerKilo) : 0;
}

function getMatch(title) {
    let teabagMatch = regexTeaBag.exec(title);
    
    if (teabagMatch !== null) {
        return [
            '',
            eval(teabagMatch[0].replace('ф/п', '').replaceAll(' ', '').replace(',', '.').replace(/x|X|х|Х/, '*')).toString(),
            'г'
        ];
    } else {
        return regex.exec(title);
    }
}

function insertUnitWeight($parent) {
    if (!$parent.attr('data-unit-weight-appended') && $parent.find('.change-weight').length) {
        let weight = `${Math.round($parent.find('.checkbox-custom__input').attr('data-unit-step') * 1000)}г`;
        $parent.find('.product-price--unit .product-price__unit').append(` (${weight})`);
        $parent.attr('data-unit-weight-appended', 1);
    }
}

function loadPaginatedProductsRecursively(url, finishCallback, errorCallback) {
    //https://devtoolstips.org/tips/en/list-all-event-listeners/ - pull element event

    $.ajax({
        async: false,
        url: url,
        success: function(data) {
            let $products = $(data).find('.catalog-list > article.catalog-item');
            if ($products.length) {
	        $('.catalog-list').append($products);
	        $products.find('.b-addToCart').addClass('b-addToCart--appended').on('click', function() {
	            $(this).closest('article').find('.catalog-item__title a').attr('target', '_blank')[0].click();
	        });
            }

            let $nextPageBtn = $(data).find('.product-pagination__item.next');
            if ($nextPageBtn.length && !$nextPageBtn.hasClass('disabled')) {
                setTimeout(() => loadPaginatedProductsRecursively($nextPageBtn.find('a').attr('href'), finishCallback, errorCallback), 1000);
            } else {
                finishCallback();
            }
        },
        error: function(jqXHR, textStatus, errorThrown) {
            alert('Error! (see console for details)');
            console.log(jqXHR, textStatus, errorThrown);
            errorCallback();
        }
    });
}

function sortCatalogByPricePerKilo() {
    $('article.banner-item').hide();
    $('.catalog-list article.catalog-item').each(function() {
        let title = $(this).find('.catalog-item__title').text().trim();
        let match = getMatch(title);
        let $priceBlock = $(this).find('> .catalog-item__bottom:not(.price_per_kilo)').first();
        $(this).attr('data-lowest-price-per-kilo', getLowestPricePerKilo($priceBlock, match !== null ? getFormatedWeight(match) : 1000));
    });

    $('article.catalog-item', '.catalog-list').sort(function (a, b) {
        var contentA = parseFloat($(a).attr('data-lowest-price-per-kilo'));
        var contentB = parseFloat($(b).attr('data-lowest-price-per-kilo'));
        return (contentA < contentB) ? -1 : (contentA > contentB) ? 1 : 0;
    }).prependTo('.catalog-list');
}

//-------------- STYLES ---------------//

GM_addStyle(/*css*/`
    .catalog-item__price-per-kilo--hidden {
        display: none !important;
    }

    .price-per-kilo_global--hidden .price_per_kilo {
        display: none !important;
    }

    .search-custom-price {
        display: flex;
        align-items: center;
        margin-bottom: 10px;
    }

    .search-custom-price_bottom,
    .search-custom-price_top,
    .search-custom-price_card {
        font-weight: 600;
        margin-right: 5px;
    }

    .search-custom-price_bottom:last-child,
    .search-custom-price_top:last-child,
    .search-custom-price_card:last-child {
        margin-right: 0px;
    }

    .search-custom-price_bottom {
        text-decoration: line-through;
        color: var(--text-grey);
    }

    .search-custom-price_top {
        color: var(--text-color);
    }

    .search-custom-price_bottom + .search-custom-price_top {
        color: var(--accent-color) !important;
    }

    .search-custom-price_card {
        width: fit-content;
        padding: 2px 4px;
        color: var(--accent-light);
        background: #28aa4e;
        border-radius: 4px;
    }

    .search-custom-price__price-per-kilo--text {
        margin-top: -10px;
        font-weight: 600;
        font-size: 12px;
        color: var(--text-color);
    }

    .product-about__price-per-kilo {
        margin-top: 15px;
        position: relative;
    }

    .product-about__price-per-kilo::before {
        content: '------- Ціна за кілограм -------';
        position: absolute;
        top: -30px;
        left: 0;
        font-weight: bold;
    }

    .catalog-item__price-per-kilo {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        margin-top: 50px !important
    }

    .catalog-item__price-per-kilo .atbcard-sale {
        position: static;
    }

    .catalog-item__price-per-kilo::before {
        content: '------- Ціна за кілограм -------';
        position: absolute;
        top: -15px;
        left: 0;
        font-weight: bold;
        transform: translateY(-100%);
    }

    .multi-results .multi-label {
        width: min-content;
    }

    .sort_by_price_pe_kilo {
        margin-right: 15px;
        color: black;
        font-size: larger;
        font-weight: 500;
    }
    .sort_by_price_pe_kilo[disabled] {
        background-color: #bffbbf;
    }

    .b-addToCart--appended .b-addToCart__basket-btn svg {
        background: red;
        border-radius: 7px;
    }

    .b-addToCart--appended .b-addToCart__btn-wrap {
        border-color: red;
    }
`);