Сейчас узнаем, какой подарок выпадет именно Вам
Крутите колесо
Как показать количество товара для каждого раздела в Тильда

Как показать количество товара для каждого раздела в Тильда

1
Создали каталог товаров с остатками
2
Вывели товары в блок магазина (в примере ST340A)
3
Вставили код на страницу в блок Т123
Библиотека для примера
<script>
const CONFIG = {
    CACHE_KEY: 'tilda_parts_cache_v1',
    CACHE_TTL: 24 * 60 * 60 * 1000,
    API_BASE: 'https://store.tildaapi.com/api/getproductslist/',
    DEBUG: false,
    TIMINGS: {
        GRID_RENDER_DELAY: 500,
        WAIT_PART_ELEMENTS: 10000,
        WAIT_XHR: 10000,
        FALLBACK_TIMEOUT: 8000,
        RETRY_DELAY: 1000,
        MAX_RETRIES: 3
    }
};

function log(...args) {
    if (CONFIG.DEBUG) console.log('[PART-TOTAL]', ...args);
}

function getRealGrids() {
    return Array.from(document.querySelectorAll('[class*="-grid-cont"]'))
        .filter(el => !Array.from(el.classList).some(cls => cls.includes('preloader')));
}

function waitForTStoreXHR(timeout = CONFIG.TIMINGS.WAIT_XHR) {
    return new Promise((resolve) => {
        if (window.tStoreXHR) {
            resolve(window.tStoreXHR);
            return;
        }
        const start = Date.now();
        const interval = setInterval(() => {
            if (window.tStoreXHR) {
                clearInterval(interval);
                resolve(window.tStoreXHR);
            } else if (Date.now() - start >= timeout) {
                clearInterval(interval);
                resolve({});
            }
        }, 200);
    });
}

function waitForPartElements(timeout = CONFIG.TIMINGS.WAIT_PART_ELEMENTS) {
    return new Promise((resolve) => {
        const start = Date.now();
        const check = () => {
            const els = document.querySelectorAll('[data-storepart-uid]');
            if (els.length > 0) {
                resolve(els);
            } else if (Date.now() - start >= timeout) {
                resolve([]);
            } else {
                setTimeout(check, 300);
            }
        };
        check();
    });
}

function flattenParts(parts, sections = {}, seenUids = new Set(), basePath = []) {
    if (!Array.isArray(parts)) return { sections, seenUids };
    
    for (const part of parts) {
        const { uid, title, path, subparts } = part;
        if (!uid || !title || seenUids.has(uid)) continue;
        
        seenUids.add(uid);
        
        const searchKeys = [String(uid)];
        if (title) searchKeys.push(title);
        
        const pathArray = Array.isArray(path) && path.length > 0 
            ? path 
            : (basePath.length > 0 ? [...basePath, title] : [title]);
        
        if (pathArray.length > 0) {
            const pathKey = pathArray.join('/@/');
            if (pathKey !== title) searchKeys.push(pathKey);
        }
        
        searchKeys.forEach(key => {
            if (!sections[key]) sections[key] = { uid: String(uid), total: 0 };
        });
        
        if (Array.isArray(subparts) && subparts.length > 0) {
            flattenParts(subparts, sections, seenUids, pathArray);
        }
    }
    return { sections, seenUids };
}

async function extractSectionsFromStore() {
    const store = await waitForTStoreXHR();
    if (!Object.keys(store).length) return {};
    
    const sections = {};
    const seenUids = new Set();
    
    Object.values(store).forEach(block => {
        try {
            if (!block?.response) return;
            const response = JSON.parse(block.response);
            if (!Array.isArray(response.parts)) return;
            flattenParts(response.parts, sections, seenUids);
        } catch (e) {
            console.error('Error parsing store response:', e.message);
        }
    });
    
    return sections;
}

function fetchPartTotal(partUid) {
    return fetch(`${CONFIG.API_BASE}?storepartuid=${partUid}`, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
    })
    .then(res => res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`))
    .then(json => ({ 
        partUid: String(partUid), 
        total: typeof json.total === 'number' ? json.total : 0 
    }))
    .catch(err => {
        console.error(`Failed to fetch total for UID ${partUid}:`, err);
        return { partUid: String(partUid), total: 0 };
    });
}

function findTargetElement(baseEl) {
    if (Array.from(baseEl.classList).some(cls => cls.includes('parts-switch-btn-all'))) {
        return baseEl;
    }
    const selectors = [
        '[class*="parts-item-title"]',
        '[class*="parts-tree-btn-title"]',
        '[class*="filter__title"]'
    ];
    for (const sel of selectors) {
        const el = baseEl.querySelector(sel);
        if (el) return el;
    }
    if (baseEl.tagName?.toLowerCase() === 'input') {
        const label = baseEl.closest('label');
        if (label) {
            const el = label.querySelector('[class*="filter-tree-label"]');
            if (el) return el;
        }
    }
    return null;
}

function updateElements(results) {
    let updatedCount = 0;
    
    results.forEach(({ name, total, uid }) => {
        const searchValues = [
            uid,
            name,
            ...(name.includes('/@/') ? [name.split('/@/').pop()] : [])
        ];
        const uniqueValues = [...new Set(searchValues.filter(v => v))];
        
        uniqueValues.forEach(value => {
            const selector = `[data-storepart-uid="${value}"]`;
            document.querySelectorAll(selector).forEach(baseEl => {
                const target = findTargetElement(baseEl);
                if (target && !target.hasAttribute('data-total-product')) {
                    target.setAttribute('data-total-product', total);
                    updatedCount++;
                }
            });
        });
    });
    return updatedCount;
}

function getCachedData() {
    try {
        const raw = localStorage.getItem(CONFIG.CACHE_KEY);
        if (!raw) return null;
        const { ts, sections, data } = JSON.parse(raw);
        if (Date.now() - ts < CONFIG.CACHE_TTL) {
            return { sections, data };
        }
    } catch (e) {}
    return null;
}

function saveToCache(sections, data) {
    try {
        localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify({
            ts: Date.now(), sections, data
        }));
    } catch (e) {}
}

async function processCatalogSections(attempt = 1) {
    const cached = getCachedData();
    if (cached) {
        const results = Object.entries(cached.data).map(([name, { total, uid }]) => ({ 
            name, total, uid 
        }));
        const updated = updateElements(results);
        if (updated === 0 && attempt < CONFIG.TIMINGS.MAX_RETRIES) {
            await new Promise(r => setTimeout(r, CONFIG.TIMINGS.RETRY_DELAY));
            return processCatalogSections(attempt + 1);
        }
        return;
    }
    
    await waitForPartElements();
    const sections = await extractSectionsFromStore();
    const entries = Object.entries(sections);
    
    if (!entries.length) {
        return;
    }
    
    const results = await Promise.all(
        entries.map(([name, { uid }]) => 
            fetchPartTotal(uid).then(res => ({ name, total: res.total, uid }))
        )
    );
    
    const cacheData = results.reduce((acc, { name, total, uid }) => {
        acc[name] = { total, uid };
        return acc;
    }, {});
    
    saveToCache(sections, cacheData);
    const updated = updateElements(results);
    
    if (updated === 0 && attempt < CONFIG.TIMINGS.MAX_RETRIES) {
        await new Promise(r => setTimeout(r, CONFIG.TIMINGS.RETRY_DELAY));
        return processCatalogSections(attempt + 1);
    }
}

document.addEventListener('DOMContentLoaded', function() {
    const grids = getRealGrids();
    
    if (!grids.length) {
        return;
    }
    
    let rendered = 0;
    const total = grids.length;
    let processed = false;
    
    const onRendered = function(e) {
        if (!e.target.matches('[class*="-grid-cont"]')) return;
        if (Array.from(e.target.classList).some(cls => cls.includes('preloader'))) return;
        rendered++;
        
        if (rendered === total && !processed) {
            processed = true;
            setTimeout(() => {
                processCatalogSections();
            }, CONFIG.TIMINGS.GRID_RENDER_DELAY);
            grids.forEach(grid => grid.removeEventListener('tStoreRendered', onRendered));
        }
    };
    
    grids.forEach(grid => grid.addEventListener('tStoreRendered', onRendered));
    
    setTimeout(() => {
        if (!processed) {
            processed = true;
            setTimeout(() => processCatalogSections(), CONFIG.TIMINGS.GRID_RENDER_DELAY);
            grids.forEach(grid => grid.removeEventListener('tStoreRendered', onRendered));
        }
    }, CONFIG.TIMINGS.FALLBACK_TIMEOUT);
});
</script>




<style>
[class*="parts-switch-btn-all"][data-total-product]:after,
[class*="parts-item-title"][data-total-product]:after,
[class*="parts-tree-btn-title"][data-total-product]:after,
[class*="filter__title"][data-total-product]:after,
[class*="filter-tree-label"][data-total-product]:after,
.parts-switch-btn-all[data-total-product]:after {
    content: attr(data-total-product);
    font-size: 0.7em;
    opacity: 0.5;
    pointer-events: none;
    padding: 2px 2px;
    white-space: nowrap;
    position: relative;
    top: -6px;
}

[class*="parts-switch-btn-all"][data-total-product] {
    justify-content: flex-start;
}

.js-store-parts-switcher,
[class*="parts-tree-btn"],
[class*="filter-tree-item"],
[class*="filter__item"],
.parts-switch-btn-all {
    position: relative;
}

.t-store__parts-item-title {
    display: flex;
    align-items: center;
    gap: 0px;
}
</style>
Made on
Tilda