2026-08-22 21:51:25 +03:00
"use strict" ;
2026-08-21 18:23:20 +03:00
const host = 'https://local.hvatilo.ru:8055' ;
2026-08-21 17:52:45 +03:00
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
function throttle ( func , wait , options ) {
var context , args , result ;
var timeout = null ;
var previous = 0 ;
if ( ! options ) options = {};
var later = function () {
previous = options . leading === false ? 0 : Date . now ();
timeout = null ;
result = func . apply ( context , args );
if ( ! timeout ) context = args = null ;
};
return function () {
var now = Date . now ();
if ( ! previous && options . leading === false ) previous = now ;
var remaining = wait - ( now - previous );
context = this ;
args = arguments ;
if ( remaining <= 0 || remaining > wait ) {
if ( timeout ) {
clearTimeout ( timeout );
timeout = null ;
}
previous = now ;
result = func . apply ( context , args );
if ( ! timeout ) context = args = null ;
} else if ( ! timeout && options . trailing !== false ) {
timeout = setTimeout ( later , remaining );
}
return result ;
};
}
function refreshToken () {
2026-08-22 21:51:25 +03:00
return fetch ( host + '/api/guest/refresh_cookie' , { method : 'POST' , credentials : 'include' })
. then ( response => {
if ( response . status === 400 ) {
return fetch ( host + '/api/guest/clear_cookie' , { method : 'POST' , credentials : 'include' })
. then ( async ( d ) => {
await new Promise ( resolve => setTimeout ( resolve , 100 ));
return initToken ();
});
}
return response . json ();
});
2026-08-21 17:52:45 +03:00
}
function initToken () {
return fetch ( host + '/api/guest/init_cookie?utm_source=hvatilo_promo' , { method : 'POST' , credentials : 'include' }). then ( response => {
return response . json ();
});
}
const requestWithRefresh = async function ( url , options ) {
return fetch ( url , {... options , credentials : 'include' })
. then ( async response => response . json ())
. then ( async data => {
if ( data . error_message === 'need_refresh' ) {
return refreshToken ()
. then (( r ) => {
console . log ( 'url' , url );
return fetch ( url , {... options , credentials : 'include' }). then ( response => response . json ());
});
}
return data ;
})
}
const replaceProduct = ( async function ( product_name ) {
return requestWithRefresh ( host + '/api/promo/replace_item' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
},
body : JSON . stringify ({ product_name })
})
. then ( async () => {
updateCart ();
})
})
const deleteProduct = ( async function ( product_name ) {
return requestWithRefresh ( host + '/api/promo/delete_item' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
},
body : JSON . stringify ({ product_name })
})
})
const updateCart = throttle ( async function () {
2026-08-22 21:51:25 +03:00
document . getElementById ( 'summary-loader' ). classList . remove ( 'hidden' );
document . getElementById ( 'summary-details' ). classList . add ( 'hidden' );
2026-08-21 17:52:45 +03:00
return requestWithRefresh ( host + '/api/promo/get_updates' , { headers : {
'Content-Type' : 'application/json'
}}). then ( d => {
if ( d . status !== 'complete' ) {
initialData_g . excess = d . payload . excess ;
initialData_g . precision = d . payload . precision ;
if ( d . status === 'clear' ) {
allProducts = [];
visibleProducts = [];
renderProducts ();
allProducts = d . payload . chunk ;
visibleProducts = d . payload . chunk ;
}
if ( d . status === 'data' ) {
d . payload . chunk . forEach ( addedProduct => {
let idx = allProducts . findIndex ( p => p . name === addedProduct . name )
if ( idx === - 1 ) {
allProducts . push ({... addedProduct , image : addedProduct . image ? . replace ( '{SIZE}' , '65,fit' )});
if ( visibleProducts . length === allProducts . length - 1 ) {
visibleProducts = allProducts ;
}
} else {
allProducts [ idx ]. n += addedProduct . n ;
if ( idx < visibleProducts . length ) {
visibleProducts [ idx ] = allProducts [ idx ];
}
}
})
}
if ( d . status !== 'waiting' ) {
renderProducts ();
}
updateCart ();
2026-08-22 21:51:25 +03:00
} else {
document . getElementsByClassName ( 'cart-footer' )[ 0 ]. classList . add ( 'visible' );
document . getElementById ( 'summary-loader' ). classList . add ( 'hidden' );
document . getElementById ( 'summary-details' ). classList . remove ( 'hidden' );
2026-08-21 17:52:45 +03:00
}
})
. catch (() => {
updateCart ();
})
}, 3000 );
const setOption = ( key , value ) => {
return requestWithRefresh ( host + '/api/promo/set_option' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
},
body : JSON . stringify ({[ key ] : value })
})
. then ( async d => {
wholeRender ( await loadInitData ());
updateCart ();
2026-08-22 21:51:25 +03:00
return d ;
})
. catch (() => {
console . log ( 'start catch' )
return new Promise (( resolve , reject ) => {
setTimeout (() => {
console . log ( 'end catch' )
reject ( 'end catch' )
}, 1000 );
})
2026-08-21 17:52:45 +03:00
})
}
2026-08-22 21:51:25 +03:00
const mockData = {
"existing" : 5432 ,
"available_groups" : [
"Приправы" ,
"Овощи" ,
"Напитки" ,
"Сыр" ,
"Птица в лотках" ,
"Сладости" ,
"Подсластители" ,
"Кисломолочные" ,
"Масло для готовки" ,
"Ягоды" ,
"Соусы" ,
"Для готовки" ,
"Горячие напитки" ,
"Засоленные" ,
"Морепродукты" ,
"Кисломолочное" ,
"Кисломолочные напитки" ,
"Алкоголь" ,
"Хлебцы" ,
"Сладкое-сладкое" ,
"Капуста" ,
"Детское питание" ,
"Овощи для готовки" ,
"Гарнир" ,
"Хлеб" ,
"Консервы" ,
"Суп" ,
"Острое" ,
"Вермишель" ,
"Рыба" ,
"Сливки" ,
"Грибы" ,
"Салат" ,
"Субпродукты" ,
"Картофель" ,
"Молочные" ,
"Морковь" ,
"Жевательная резинка" ,
"Фрукты" ,
"Зелень" ,
"Специальное питание" ,
"Яйца"
],
"initial_groups" : [
"Овощи" ,
"Овощи для готовки" ,
"Молочные" ,
"Птица в лотках" ,
"Гарнир" ,
"Хлеб" ,
"Кисломолочные" ,
"Яйца" ,
"Рыба" ,
"Масло для готовки"
],
"days" : 7 ,
"users" : [
{
"id" : - 1 ,
"name" : "Иван" ,
"gender" : "m" ,
"age" : 30 ,
"weight" : 87 ,
"height" : 180 ,
"avoid" : [
"Детское питание"
],
"favorite" : []
},
{
"id" : - 2 ,
"name" : "Мария" ,
"gender" : "f" ,
"age" : 28 ,
"weight" : 65 ,
"height" : 160 ,
"avoid" : [
"Детское питание"
],
"favorite" : [
"Яблоко"
]
},
{
"id" : - 3 ,
"name" : "Пётр" ,
"gender" : "m" ,
"age" : 10 ,
"weight" : 45 ,
"height" : 140 ,
"avoid" : [
"Алкоголь" ,
"Засоленные" ,
"Острое"
],
"favorite" : []
}
],
"cart" : [
[
{
"name" : "Овощи для жарки, 1кг" ,
"n" : 1200 ,
"price" : 23.799 ,
"total_price" : 142.79399999999998 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/64\/79\/226479.jpg" ,
"unit" : "g"
},
{
"name" : "Картофель фри, 1кг" ,
"n" : 300 ,
"price" : 21.799 ,
"total_price" : 43.598 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/66\/06\/226606.jpg" ,
"unit" : "g"
},
{
"name" : "Свекла соломка уп, 300-500г " ,
"n" : 400 ,
"price" : 27.99 ,
"total_price" : 111.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/04\/384604.jpg" ,
"unit" : "g"
},
{
"name" : "Молоко ультрапастеризованное Ам-Ам Мамина забота обогащённое витамином C, 8+ месяцев, 205г " ,
"n" : 1 ,
"price" : 47.49 ,
"total_price" : 47.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/23\/26\/4852326.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь Волшебное дерево спагетти болоньезе, 30г" ,
"n" : 1 ,
"price" : 72.49 ,
"total_price" : 72.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/98\/53\/4399853.jpg" ,
"unit" : "pcs"
},
{
"name" : "Хренодер ФЭГ ядрёный, 240г" ,
"n" : 1 ,
"price" : 131.99 ,
"total_price" : 131.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/40\/74\/274074.jpg" ,
"unit" : "pcs"
},
{
"name" : "Огурцы свежие с укропом, вес" ,
"n" : 2000 ,
"price" : 41.99 ,
"total_price" : 419.90000000000003 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/36\/73\/483673.jpg" ,
"unit" : "g"
},
{
"name" : "Продукт кисломолочный творожный Exponenta Топленое молоко 0%, 250г" ,
"n" : 2 ,
"price" : 191.99 ,
"total_price" : 383.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/03\/28\/4180328.jpg" ,
"unit" : "pcs"
}
],
[
{
"name" : "Мороженое сливочное Петрохолод Ленинградское Ванильное в шоколадной глазури, 80г" ,
"n" : 2 ,
"price" : 47.99 ,
"total_price" : 95.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/17\/67\/2091767.jpg" ,
"unit" : "pcs"
},
{
"name" : "Капуста белокочанная резаная уп, 400-800г " ,
"n" : 1800 ,
"price" : 31.99 ,
"total_price" : 383.88 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/60\/12\/4476012.jpg" ,
"unit" : "g"
},
{
"name" : "Крем-суп протеиновый Bionova Томатный, 20г " ,
"n" : 1 ,
"price" : 71.99 ,
"total_price" : 71.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/66\/98\/546698.jpg" ,
"unit" : "pcs"
},
{
"name" : "Креветки королевские варёно-мороженые Agama очищенные, 300г " ,
"n" : 1 ,
"price" : 769.99 ,
"total_price" : 769.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/13\/54\/1221354.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чипсы нори Naitori со вкусом краба, 3г" ,
"n" : 1 ,
"price" : 54.99 ,
"total_price" : 54.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/65\/19\/4276519.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салат Белая Дача Botanicum мини руккола, айсберг, романо, 75г" ,
"n" : 1 ,
"price" : 184.99 ,
"total_price" : 184.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/51\/87\/4055187.jpg" ,
"unit" : "pcs"
},
{
"name" : "Каша рисовая безмолочная HiPP Organic фрукты-злаки с 5 месяцев, 190г" ,
"n" : 1 ,
"price" : 216.99 ,
"total_price" : 216.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/48\/03\/4224803.jpg" ,
"unit" : "pcs"
},
{
"name" : "Стебель сельдерея в нарезке, вес" ,
"n" : 300 ,
"price" : 45.999 ,
"total_price" : 137.997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/28\/08\/422808.jpg" ,
"unit" : "g"
},
{
"name" : "Йогурт двухслойный Савушкин манго 2%, 120г" ,
"n" : 1 ,
"price" : 32.99 ,
"total_price" : 32.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/67\/70\/4596770.jpg" ,
"unit" : "pcs"
}
],
[
{
"name" : "Котлеты куриные Троекурово, 500г" ,
"n" : 3 ,
"price" : 109.99 ,
"total_price" : 329.96999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/71\/57\/407157.jpg" ,
"unit" : "pcs"
},
{
"name" : "Макаронные изделия Ваш выбор Перья, 400г" ,
"n" : 2 ,
"price" : 19.99 ,
"total_price" : 39.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/27\/72\/3422772.jpg" ,
"unit" : "pcs"
},
{
"name" : "Хлеб украинский новый Пеко в упаковке, 650г" ,
"n" : 2 ,
"price" : 25.99 ,
"total_price" : 51.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/27\/15\/412715.jpg" ,
"unit" : "pcs"
},
{
"name" : "Фарш из минтая охлаждённый, вес" ,
"n" : 600 ,
"price" : 59.999 ,
"total_price" : 359.994 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/82\/35\/318235.jpg" ,
"unit" : "g"
},
{
"name" : "Мороженое сливочное На сливочках Ежевика-Малина, 85г" ,
"n" : 6 ,
"price" : 92.99 ,
"total_price" : 557.9399999999999 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/62\/38\/4816238.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салат Белая Дача Пряная рукола, 50г" ,
"n" : 3 ,
"price" : 216.99 ,
"total_price" : 650.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/37\/03\/4663703.jpg" ,
"unit" : "pcs"
},
{
"name" : "Семена горчицы Мастер Дак, 15 г" ,
"n" : 1 ,
"price" : 6.49 ,
"total_price" : 6.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/82\/27\/4858227.jpg" ,
"unit" : "pcs"
},
{
"name" : "Конфета 35 со вкусом Шоколада, 20г" ,
"n" : 2 ,
"price" : 19.99 ,
"total_price" : 39.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/54\/45\/3505445.jpg" ,
"unit" : "pcs"
},
{
"name" : "Сыр Зеленый в порошке с пажитником 1,4%, 50г " ,
"n" : 2 ,
"price" : 173.99 ,
"total_price" : 347.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/33\/28\/3943328.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь грибная быстрозамороженная И Зимой И Летом, 300г" ,
"n" : 1 ,
"price" : 351.99 ,
"total_price" : 351.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/82\/32\/4838232.jpg" ,
"unit" : "pcs"
},
{
"name" : "Паста соевая Miso тёмная, 1кг" ,
"n" : 1 ,
"price" : 449.99 ,
"total_price" : 449.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/43\/13\/424313.jpg" ,
"unit" : "pcs"
},
{
"name" : "Клюква дикорастущая Глобус, 300г" ,
"n" : 1 ,
"price" : 609.99 ,
"total_price" : 609.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/90\/39\/3389039.jpg" ,
"unit" : "pcs"
},
{
"name" : "Соус соевый Тай-Со Классический, 250мл" ,
"n" : 1 ,
"price" : 69.99 ,
"total_price" : 69.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/92\/03\/2489203.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь овощная Мексиканская, 1кг" ,
"n" : 1200 ,
"price" : 27.399 ,
"total_price" : 164.394 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/63\/91\/226391.jpg" ,
"unit" : "g"
},
{
"name" : "Мороженое сливочное Ля Фам Сладкий лимон, 70г" ,
"n" : 1 ,
"price" : 51.49 ,
"total_price" : 51.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/69\/64\/4296964.jpg" ,
"unit" : "pcs"
},
{
"name" : "Каша мультизлаковая безмолочная Сады Придонья яблоко-банан-малина с 6 месяцев, 0,125л" ,
"n" : 1 ,
"price" : 32.99 ,
"total_price" : 32.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/34\/03\/713403.jpg" ,
"unit" : "pcs"
},
{
"name" : "Кальмар гигантский Глобус без кожицы, 185г" ,
"n" : 1 ,
"price" : 199.99 ,
"total_price" : 199.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/27\/07\/4752707.jpg" ,
"unit" : "pcs"
},
{
"name" : "Треска тихоокеанская мороженая Глобус филе-кусок с кожей, 500г" ,
"n" : 1 ,
"price" : 709.99 ,
"total_price" : 709.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/99\/73\/1389973.jpg" ,
"unit" : "pcs"
},
{
"name" : "Тунец полосатый натуральный Морской котик стерилизованный филе, 170г" ,
"n" : 1 ,
"price" : 203.99 ,
"total_price" : 203.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/86\/75\/508675.jpg" ,
"unit" : "pcs"
},
{
"name" : "Крем-суп протеиновый Bionova Сырный, 20г " ,
"n" : 1 ,
"price" : 72.99 ,
"total_price" : 72.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/67\/00\/546700.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салат Tango mix Белая Дача рукола, мангольд, 50г" ,
"n" : 1 ,
"price" : 218.99 ,
"total_price" : 218.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/11\/08\/4401108.jpg" ,
"unit" : "pcs"
},
{
"name" : "Картофель отварной уп, 500-650г " ,
"n" : 2400 ,
"price" : 35.99 ,
"total_price" : 431.88 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/60\/81\/4476081.jpg" ,
"unit" : "g"
},
{
"name" : "Йогурт греческий Venn's натуральный 0,1%, 130г " ,
"n" : 1 ,
"price" : 56.99 ,
"total_price" : 56.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/02\/47\/4160247.jpg" ,
"unit" : "pcs"
}
],
[
{
"name" : "Лук репчатый, вес" ,
"n" : 900 ,
"price" : 4.899 ,
"total_price" : 44.091 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/74\/39\/287439.jpg" ,
"unit" : "g"
},
{
"name" : "Какао+ Экологика стевия + пребиотик, 125г" ,
"n" : 1 ,
"price" : 569.99 ,
"total_price" : 569.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/64\/31\/846431.jpg" ,
"unit" : "pcs"
},
{
"name" : "Дрожжи хлебопекарные Ваш выбор сухие, 12г" ,
"n" : 3 ,
"price" : 8.99 ,
"total_price" : 26.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/79\/28\/4157928.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь овощная Лечо, 1кг" ,
"n" : 1200 ,
"price" : 27.299 ,
"total_price" : 163.79399999999998 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/64\/80\/226480.jpg" ,
"unit" : "g"
},
{
"name" : "Огурцы солёные Традиции вкуса, 300г" ,
"n" : 1 ,
"price" : 121.99 ,
"total_price" : 121.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/81\/54\/868154.jpg" ,
"unit" : "pcs"
},
{
"name" : "Маршмеллоу Мини Guandy белый ванильный, 75г" ,
"n" : 1 ,
"price" : 94.49 ,
"total_price" : 94.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/86\/03\/1578603.jpg" ,
"unit" : "pcs"
},
{
"name" : "Перец красный Мастер Дак молотый, 10г" ,
"n" : 1 ,
"price" : 8.99 ,
"total_price" : 8.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/23\/41\/252341.jpg" ,
"unit" : "pcs"
},
{
"name" : "Молоко сгущенное цельное Алексеевское с сахаром, стик 8,5%, 7г " ,
"n" : 1 ,
"price" : 4.99 ,
"total_price" : 4.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/56\/82\/4095682.jpg" ,
"unit" : "pcs"
},
{
"name" : "Овощное ассорти острое ФЭГ, 800г" ,
"n" : 1 ,
"price" : 195.99 ,
"total_price" : 195.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/05\/48\/300548.jpg" ,
"unit" : "pcs"
},
{
"name" : "Концентрированный сухой бульон Ассорти Kruglie Sutki Коллаген, 75г " ,
"n" : 1 ,
"price" : 749.99 ,
"total_price" : 749.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/10\/08\/prd_810461008.jpg" ,
"unit" : "pcs"
},
{
"name" : "Хлебцы экструзионные Три Грации с томатом и прованскими травами, 55г" ,
"n" : 1 ,
"price" : 43.49 ,
"total_price" : 43.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/63\/61\/4106361.jpg" ,
"unit" : "pcs"
},
{
"name" : "Соус соевый Yatomi, 30г " ,
"n" : 1 ,
"price" : 31.99 ,
"total_price" : 31.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/83\/72\/3778372.jpg" ,
"unit" : "pcs"
},
{
"name" : "Помидоры протертые Burcu, 350г " ,
"n" : 1 ,
"price" : 204.99 ,
"total_price" : 204.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/09\/82\/4400982.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чеснок резаный LiViAnTa, 100г " ,
"n" : 1 ,
"price" : 203.99 ,
"total_price" : 203.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/89\/78\/4818978.jpg" ,
"unit" : "pcs"
},
{
"name" : "Творог обезжиренный Рузское молоко, 200г" ,
"n" : 1 ,
"price" : 145.99 ,
"total_price" : 145.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/06\/644606.jpg" ,
"unit" : "pcs"
}
],
[
{
"name" : "Молоко из Вологды с витаминами и йодом для хороших оценок 3,2%, 200мл" ,
"n" : 9 ,
"price" : 39.99 ,
"total_price" : 359.91 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/02\/64\/4160264.jpg" ,
"unit" : "pcs"
},
{
"name" : "Продукт йогуртный Нежный с соком клубники 1,2%, 100г " ,
"n" : 8 ,
"price" : 23.49 ,
"total_price" : 187.92 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/50\/67\/4895067.jpg" ,
"unit" : "pcs"
},
{
"name" : "Яйца куриные столовые Синявинское С0 белое, 30г" ,
"n" : 2 ,
"price" : 389.99 ,
"total_price" : 779.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/38\/51\/4233851.jpg" ,
"unit" : "pcs"
},
{
"name" : "Шеи цыплят-бройлеров охлаждённые Троекурово, 500г" ,
"n" : 3 ,
"price" : 109.99 ,
"total_price" : 329.96999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/79\/84\/3967984.jpg" ,
"unit" : "pcs"
},
{
"name" : "Хлебцы хрустящие Ого! Столовые, 75г" ,
"n" : 4 ,
"price" : 32.99 ,
"total_price" : 131.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/25\/46\/3432546.jpg" ,
"unit" : "pcs"
},
{
"name" : "Творог обезжиренный Козельский Живой, 180г" ,
"n" : 2 ,
"price" : 103.99 ,
"total_price" : 207.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/24\/06\/1502406.jpg" ,
"unit" : "pcs"
},
{
"name" : "Мята перечная сушёная Приправыч, 6г" ,
"n" : 1 ,
"price" : 23.99 ,
"total_price" : 23.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/91\/92\/4359192.jpg" ,
"unit" : "pcs"
},
{
"name" : "Микрозелень редиса санго MaxiFermer, 50г " ,
"n" : 4 ,
"price" : 215.99 ,
"total_price" : 863.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/54\/88\/4385488.jpg" ,
"unit" : "pcs"
},
{
"name" : "Перец Mikado Халапеньо резанный зеленый, 350мл" ,
"n" : 1 ,
"price" : 217.99 ,
"total_price" : 217.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/78\/05\/4887805.jpg" ,
"unit" : "pcs"
},
{
"name" : "Ветчина Сочная Мираторг нарезка, 160г" ,
"n" : 1 ,
"price" : 179.99 ,
"total_price" : 179.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/04\/69\/3460469.jpg" ,
"unit" : "pcs"
},
{
"name" : "Лук зелёный Глобус перо, 250г" ,
"n" : 1 ,
"price" : 171.99 ,
"total_price" : 171.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/19\/63\/271963.jpg" ,
"unit" : "pcs"
},
{
"name" : "Щавель Богослово Арго свежий, 100г" ,
"n" : 3 ,
"price" : 217.99 ,
"total_price" : 653.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/77\/01\/277701.jpg" ,
"unit" : "pcs"
},
{
"name" : "Яблоки Айдаред, вес" ,
"n" : 1600 ,
"price" : 20.999000000000002 ,
"total_price" : 167.99200000000002 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/10\/40\/3991040.jpg" ,
"unit" : "g"
},
{
"name" : "Яблочные слайсы Яблоков, 25г" ,
"n" : 3 ,
"price" : 96.49 ,
"total_price" : 289.46999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/32\/15\/3693215.jpg" ,
"unit" : "pcs"
},
{
"name" : "Борщ быстрорастворимый Кнорр Чашка Супа с сухариками, 14,8г" ,
"n" : 2 ,
"price" : 28.49 ,
"total_price" : 56.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/70\/36\/4457036.jpg" ,
"unit" : "pcs"
},
{
"name" : "Весенние овощи, 1кг" ,
"n" : 8100 ,
"price" : 29.199 ,
"total_price" : 788.373 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/64\/70\/226470.jpg" ,
"unit" : "g"
},
{
"name" : "Васаби Глобус, 5г" ,
"n" : 2 ,
"price" : 19.99 ,
"total_price" : 39.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/77\/17\/4757717.jpg" ,
"unit" : "pcs"
},
{
"name" : "Желе Jele Beautie cо вкусом клубники, 140г" ,
"n" : 2 ,
"price" : 169.99 ,
"total_price" : 339.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/15\/40\/4661540.jpg" ,
"unit" : "pcs"
},
{
"name" : "Шампанское Champagne M. Ferat & Fils Brut Blanc de Blancs Premier Cru белое брют 12% алк., Франция, 750мл" ,
"n" : 1 ,
"price" : 8869.99 ,
"total_price" : 8869.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/44\/86\/4704486.jpg" ,
"unit" : "pcs"
},
{
"name" : "Пастила Пастилушка яблоко-вишня, 1кг" ,
"n" : 50 ,
"price" : 73.999 ,
"total_price" : 73.999 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/44\/09\/4274409.jpg" ,
"unit" : "g"
},
{
"name" : "Перец Халапеньо Kühne резаный, 330г " ,
"n" : 1 ,
"price" : 478.99 ,
"total_price" : 478.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/51\/55\/375155.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салатный микс Браво Белая Дача Шпинат, мангольд, рукола, 50г" ,
"n" : 1 ,
"price" : 213.99 ,
"total_price" : 213.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/36\/97\/4663697.jpg" ,
"unit" : "pcs"
},
{
"name" : "Закуска Пиканта Астраханская, 460г" ,
"n" : 1 ,
"price" : 161.99 ,
"total_price" : 161.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/14\/17\/3421417.jpg" ,
"unit" : "pcs"
},
{
"name" : "Семя горчицы Приправыч, 20г" ,
"n" : 1 ,
"price" : 13.49 ,
"total_price" : 13.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/91\/74\/4359174.jpg" ,
"unit" : "pcs"
},
{
"name" : "Напиток спортивный ProteinRex BCAA 6000мг бабл-гам, 500мл" ,
"n" : 2 ,
"price" : 109.99 ,
"total_price" : 219.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/00\/15\/4740015.jpg" ,
"unit" : "pcs"
},
{
"name" : "Дрожжи универсальные Dr. Bakers быстродействующие, 7г" ,
"n" : 1 ,
"price" : 29.99 ,
"total_price" : 29.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/22\/31\/3612231.jpg" ,
"unit" : "pcs"
},
{
"name" : "Сироп низкокалорийный Mr. Djemius Zero кленовый без сахара, 330г" ,
"n" : 1 ,
"price" : 351.99 ,
"total_price" : 351.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/40\/14\/4314014.jpg" ,
"unit" : "pcs"
},
{
"name" : "Тыква уп, 0,4-1кг" ,
"n" : 800 ,
"price" : 55.99 ,
"total_price" : 223.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/18\/37\/411837.jpg" ,
"unit" : "g"
},
{
"name" : "Ветчина варёная по-клински Клинский Для тостов, нарезка, 120г" ,
"n" : 2 ,
"price" : 121.99 ,
"total_price" : 243.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/72\/11\/3007211.jpg" ,
"unit" : "pcs"
},
{
"name" : "Шиповник сушёный ФруктОрешки, 200г" ,
"n" : 1 ,
"price" : 106.99 ,
"total_price" : 106.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/58\/79\/255879.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чипсы нори Sen Soy Оригинал, 4,5г " ,
"n" : 3 ,
"price" : 81.49 ,
"total_price" : 244.46999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/21\/64\/4042164.jpg" ,
"unit" : "pcs"
},
{
"name" : "Грибное ассорти в маринаде Lutik, 530г " ,
"n" : 1 ,
"price" : 264.99 ,
"total_price" : 264.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/72\/51\/3497251.jpg" ,
"unit" : "pcs"
},
{
"name" : "Морковь отварная кубик уп, 450-550г " ,
"n" : 1200 ,
"price" : 35.99 ,
"total_price" : 215.94 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/13\/384613.jpg" ,
"unit" : "g"
},
{
"name" : "Баклажаны резаные, вес" ,
"n" : 800 ,
"price" : 47.999 ,
"total_price" : 191.996 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/28\/01\/422801.jpg" ,
"unit" : "g"
},
{
"name" : "Гречка Preston с мясом, 40г " ,
"n" : 1 ,
"price" : 32.49 ,
"total_price" : 32.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/55\/57\/1225557.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салака мороженая, вес" ,
"n" : 100 ,
"price" : 31.599 ,
"total_price" : 31.599 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/49\/13\/264913.jpg" ,
"unit" : "g"
},
{
"name" : "Печенье StMichel сливочное с шоколадом, 132г" ,
"n" : 1 ,
"price" : 519.99 ,
"total_price" : 519.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/22\/80\/4842280.jpg" ,
"unit" : "pcs"
},
{
"name" : "Дрожжи Парфэ Декор быстродействующие, 11г" ,
"n" : 1 ,
"price" : 18.49 ,
"total_price" : 18.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/83\/18\/4868318.jpg" ,
"unit" : "pcs"
},
{
"name" : "Микс водорослей Морская капуста с фукусом Здоровье для салата, 400г" ,
"n" : 1 ,
"price" : 182.99 ,
"total_price" : 182.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/29\/00\/1582900.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чипсы нори Глобус Токи поки, 5г" ,
"n" : 1 ,
"price" : 62.99 ,
"total_price" : 62.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/57\/65\/4435765.jpg" ,
"unit" : "pcs"
},
{
"name" : "Шиповник сушёный Isfarafood отборный, 500г " ,
"n" : 1 ,
"price" : 238.99 ,
"total_price" : 238.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/08\/01\/700801.jpg" ,
"unit" : "pcs"
},
{
"name" : "Морковь резаная палочками уп, 200-300г " ,
"n" : 100 ,
"price" : 39.99 ,
"total_price" : 39.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/01\/384601.jpg" ,
"unit" : "g"
},
{
"name" : "Молоко Parmalat 0,5%, 1л" ,
"n" : 1 ,
"price" : 149.99 ,
"total_price" : 149.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/21\/68\/3872168.jpg" ,
"unit" : "pcs"
}
],
[
{
"name" : "Путассу замороженная неразделанная, вес" ,
"n" : 2000 ,
"price" : 16.899 ,
"total_price" : 168.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/91\/35\/4289135.jpg" ,
"unit" : "g"
},
{
"name" : "Приправа для гриля Мастер Дак, 15г" ,
"n" : 2 ,
"price" : 14.49 ,
"total_price" : 28.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/53\/55\/4725355.jpg" ,
"unit" : "pcs"
},
{
"name" : "Паста соевая мисо Wanjia Shinshumiso светлая, 1кг" ,
"n" : 1 ,
"price" : 458.99 ,
"total_price" : 458.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/26\/72\/3832672.jpg" ,
"unit" : "pcs"
},
{
"name" : "Ламинария Вегана, 150г" ,
"n" : 1 ,
"price" : 629.99 ,
"total_price" : 629.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/85\/35\/148535.jpg" ,
"unit" : "pcs"
},
{
"name" : "Псиллиум Look Green, 120г " ,
"n" : 1 ,
"price" : 539.99 ,
"total_price" : 539.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/87\/76\/4838776.jpg" ,
"unit" : "pcs"
},
{
"name" : "Какао-напиток растворимый Шокомишка, 20*14г" ,
"n" : 4 ,
"price" : 15.49 ,
"total_price" : 61.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/30\/61\/4773061.jpg" ,
"unit" : "pcs"
},
{
"name" : "Соус Терияки Yatomi, 30г " ,
"n" : 5 ,
"price" : 19.99 ,
"total_price" : 99.94999999999999 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/75\/26\/3607526.jpg" ,
"unit" : "pcs"
},
{
"name" : "Васаби Yatomi, 5г " ,
"n" : 3 ,
"price" : 21.49 ,
"total_price" : 64.47 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/83\/74\/3778374.jpg" ,
"unit" : "pcs"
},
{
"name" : "Шампиньоны резаные, 1кг" ,
"n" : 2000 ,
"price" : 31.999000000000002 ,
"total_price" : 319.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/66\/05\/226605.jpg" ,
"unit" : "g"
},
{
"name" : "Халапеньо красный Глобус, 360г" ,
"n" : 2 ,
"price" : 146.99 ,
"total_price" : 293.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/45\/28\/4324528.jpg" ,
"unit" : "pcs"
},
{
"name" : "Капуста квашеная, вес" ,
"n" : 200 ,
"price" : 39.99 ,
"total_price" : 79.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/07\/57\/470757.jpg" ,
"unit" : "g"
},
{
"name" : "Крем-суп протеиновый Bionova Грибной, 20г " ,
"n" : 3 ,
"price" : 65.49 ,
"total_price" : 196.46999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/66\/96\/546696.jpg" ,
"unit" : "pcs"
},
{
"name" : "Молоко для питания детей Эконива с 3 лет 3,2%, 200мл" ,
"n" : 8 ,
"price" : 39.99 ,
"total_price" : 319.92 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/70\/45\/617045.jpg" ,
"unit" : "pcs"
},
{
"name" : "Йогурт Простоквашино с клубникой 2,9%, 110г " ,
"n" : 4 ,
"price" : 29.99 ,
"total_price" : 119.96 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/42\/39\/3474239.jpg" ,
"unit" : "pcs"
},
{
"name" : "Конфеты вафельные Обыкновенное чудо сливочное, 55г" ,
"n" : 2 ,
"price" : 39.49 ,
"total_price" : 78.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/99\/144699.jpg" ,
"unit" : "pcs"
},
{
"name" : "Макаронные изделия Ваш выбор Рожки, 400г" ,
"n" : 2 ,
"price" : 18.99 ,
"total_price" : 37.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/20\/43\/3952043.jpg" ,
"unit" : "pcs"
},
{
"name" : "Огурцы соленые корнишоны, вес" ,
"n" : 200 ,
"price" : 45.99 ,
"total_price" : 91.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/86\/07\/488607.jpg" ,
"unit" : "g"
},
{
"name" : "Тефтели из мяса цыплёнка-бройлера Троекурово, 350г" ,
"n" : 1 ,
"price" : 139.99 ,
"total_price" : 139.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/85\/06\/3068506.jpg" ,
"unit" : "pcs"
},
{
"name" : "Мороженое сливочное Ла-ла-бу-бу стакан вафельный шоко-бабл с шариками, 70г" ,
"n" : 2 ,
"price" : 81.49 ,
"total_price" : 162.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/63\/22\/4816322.jpg" ,
"unit" : "pcs"
},
{
"name" : "Пастила Пастилушка кусочки, 40г" ,
"n" : 1 ,
"price" : 36.49 ,
"total_price" : 36.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/44\/11\/4274411.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь для пудинга Haas Шоколадный, 40г " ,
"n" : 1 ,
"price" : 38.99 ,
"total_price" : 38.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/71\/91\/3927191.jpg" ,
"unit" : "pcs"
},
{
"name" : "Батончик фруктовый Pikki Чернослив, 25г " ,
"n" : 1 ,
"price" : 54.49 ,
"total_price" : 54.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/59\/84\/915984.jpg" ,
"unit" : "pcs"
},
{
"name" : "Стрипсы куриные Троекурово острые, 350г" ,
"n" : 1 ,
"price" : 199.99 ,
"total_price" : 199.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/23\/28\/1342328.jpg" ,
"unit" : "pcs"
},
{
"name" : "Молоко обезжиренное Viola UHT 0,05%, 971мл" ,
"n" : 2 ,
"price" : 149.99 ,
"total_price" : 299.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/20\/70\/3902070.jpg" ,
"unit" : "pcs"
},
{
"name" : "Свёкла отварная уп, 500-600г " ,
"n" : 400 ,
"price" : 35.99 ,
"total_price" : 71.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/64\/71\/1156471.jpg" ,
"unit" : "g"
}
],
[
{
"name" : "Хлеб пшеничный белый Пеко, 380г" ,
"n" : 1 ,
"price" : 21.99 ,
"total_price" : 21.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/90\/88\/3829088.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чипсы из морской капусты Kakao Friends классические, в ассортименте, 2г" ,
"n" : 7 ,
"price" : 59.49 ,
"total_price" : 416.43 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/15\/37\/3801537.jpg" ,
"unit" : "pcs"
},
{
"name" : "Цикорий растворимый Elite без кофеина, 75г " ,
"n" : 1 ,
"price" : 76.99 ,
"total_price" : 76.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/11\/03\/2931103.jpg" ,
"unit" : "pcs"
},
{
"name" : "Пастила Зеленика финиковая, 5шт, 60г" ,
"n" : 1 ,
"price" : 88.49 ,
"total_price" : 88.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/68\/34\/4736834.jpg" ,
"unit" : "pcs"
},
{
"name" : "Протертая мякоть томатов Пиканта с базиликом Пассата, 220г" ,
"n" : 2 ,
"price" : 104.99 ,
"total_price" : 209.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/25\/96\/4762596.jpg" ,
"unit" : "pcs"
},
{
"name" : "Салат Латук свежий листовой в горшочке, 100г" ,
"n" : 3 ,
"price" : 82.49 ,
"total_price" : 247.46999999999997 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/98\/29\/4979829.jpg" ,
"unit" : "pcs"
},
{
"name" : "Грибы лисички сухие Грибной дождь, 30г" ,
"n" : 1 ,
"price" : 483.99 ,
"total_price" : 483.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/65\/56\/4816556.jpg" ,
"unit" : "pcs"
},
{
"name" : "Микрозелень подсолнечника MaxiFermer, 50г " ,
"n" : 2 ,
"price" : 215.99 ,
"total_price" : 431.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/54\/92\/4385492.jpg" ,
"unit" : "pcs"
},
{
"name" : "Карамель Кремка с молочным вкусом, вес" ,
"n" : 90 ,
"price" : 37.39 ,
"total_price" : 112.17 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/80\/91\/4468091.jpg" ,
"unit" : "g"
},
{
"name" : "Продукт йогуртный Сливочное лакомство Fruttis в ассортименте: Клубника, Персик 5%, 115г " ,
"n" : 5 ,
"price" : 36.49 ,
"total_price" : 182.45000000000002 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/33\/89\/4143389.jpg" ,
"unit" : "pcs"
},
{
"name" : "Творог мягкий обезжиренный Простоквашино, 120г" ,
"n" : 3 ,
"price" : 47.99 ,
"total_price" : 143.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/00\/33\/4260033.jpg" ,
"unit" : "pcs"
},
{
"name" : "Чипсы нори Naitori со вкусом кимчи, 3г" ,
"n" : 3 ,
"price" : 52.99 ,
"total_price" : 158.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/65\/21\/4276521.jpg" ,
"unit" : "pcs"
},
{
"name" : "Набор для тушения из цыплят-бройлеров Верхневолжская птицефабрика уп, 0,8-1,1кг" ,
"n" : 2500 ,
"price" : 99.99 ,
"total_price" : 999.9 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/82\/33\/3168233.jpg" ,
"unit" : "g"
},
{
"name" : "Хлеб Станичный, половинка, 350г" ,
"n" : 1 ,
"price" : 47.49 ,
"total_price" : 47.49 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/36\/97\/403697.jpg" ,
"unit" : "pcs"
},
{
"name" : "Паприка Мастер Дак молотая, 10г" ,
"n" : 2 ,
"price" : 9.99 ,
"total_price" : 19.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/23\/43\/252343.jpg" ,
"unit" : "pcs"
},
{
"name" : "Смесь компотная, 1кг" ,
"n" : 450 ,
"price" : 39.999 ,
"total_price" : 119.99700000000001 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/58\/02\/4235802.jpg" ,
"unit" : "g"
},
{
"name" : "Лук зелёный Глобус, 100г" ,
"n" : 2 ,
"price" : 89.99 ,
"total_price" : 179.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/09\/59\/380959.jpg" ,
"unit" : "pcs"
},
{
"name" : "Суп белковый Ironman Fit куриный с ароматными травами, 20г" ,
"n" : 2 ,
"price" : 93.49 ,
"total_price" : 186.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/75\/87\/1867587.jpg" ,
"unit" : "pcs"
},
{
"name" : "Цветная капуста, 1кг" ,
"n" : 1200 ,
"price" : 34.599000000000004 ,
"total_price" : 207.59400000000002 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/74\/50\/377450.jpg" ,
"unit" : "g"
},
{
"name" : "Молоко из Вологды для детей с 3 лет 3,2%, 200мл" ,
"n" : 3 ,
"price" : 39.99 ,
"total_price" : 119.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/02\/65\/4160265.jpg" ,
"unit" : "pcs"
},
{
"name" : "Отруби овсяные Мистраль Энергия и бодрость, 30г" ,
"n" : 1 ,
"price" : 59.99 ,
"total_price" : 59.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/50\/98\/1785098.jpg" ,
"unit" : "pcs"
},
{
"name" : "Икра трески Беринг подкопченная, 125г" ,
"n" : 1 ,
"price" : 168.99 ,
"total_price" : 168.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/05\/68\/260568.jpg" ,
"unit" : "pcs"
},
{
"name" : "Ветчина Мясной Дом Бородина в синюге уп, 0,9-1,1кг" ,
"n" : 50 ,
"price" : 999.99 ,
"total_price" : 999.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/73\/17\/3937317.jpg" ,
"unit" : "g"
},
{
"name" : "Продукт йогуртный Нежный с соком вишни 1,2%, 100г " ,
"n" : 2 ,
"price" : 23.99 ,
"total_price" : 47.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/50\/63\/4895063.jpg" ,
"unit" : "pcs"
},
{
"name" : "Хамса пряного посола, вес" ,
"n" : 100 ,
"price" : 29.99 ,
"total_price" : 29.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/26\/37\/322637.jpg" ,
"unit" : "g"
},
{
"name" : "Печенье протеиновое Butter Wave сливочное, 36г " ,
"n" : 1 ,
"price" : 103.99 ,
"total_price" : 103.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/62\/32\/3786232.jpg" ,
"unit" : "pcs"
},
{
"name" : "Килька пряного посола, вес" ,
"n" : 50 ,
"price" : 34.99 ,
"total_price" : 34.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/27\/91\/422791.jpg" ,
"unit" : "g"
},
{
"name" : "Масло сливочное Лебедяньмолоко Крестьянское 72,5%, 180г " ,
"n" : 1 ,
"price" : 169.99 ,
"total_price" : 169.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/19\/38\/4461938.jpg" ,
"unit" : "pcs"
},
{
"name" : "Отбивная из индейки охлажденная Пава Пава, 300г" ,
"n" : 3 ,
"price" : 309.99 ,
"total_price" : 929.97 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/48\/32\/3844832.jpg" ,
"unit" : "pcs"
},
{
"name" : "Свёкла отварная соломка, вес" ,
"n" : 450 ,
"price" : 35.999 ,
"total_price" : 107.99700000000001 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/60\/65\/4476065.jpg" ,
"unit" : "g"
},
{
"name" : "Лук красный, кольца уп, 400-600г " ,
"n" : 200 ,
"price" : 34.99 ,
"total_price" : 69.98 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/46\/03\/384603.jpg" ,
"unit" : "g"
},
{
"name" : "Крупа из полбы Чёрный хлеб дроблёная, 500г" ,
"n" : 1 ,
"price" : 285.99 ,
"total_price" : 285.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/96\/65\/689665.jpg" ,
"unit" : "pcs"
},
{
"name" : "Молоко безлактозное Protein Max Molvest 0,5%, 450г " ,
"n" : 1 ,
"price" : 114.99 ,
"total_price" : 114.99 ,
"currency" : "rub" ,
"image" : "https:\/\/hvatilo.ru\/static\/retail\/ru\/image.globus\/{SIZE}\/mobile\/26\/27\/4872627.jpg" ,
"unit" : "pcs"
}
]
],
"excess" : 0.5362191665220106 ,
"precision" : 98.14517264634198
};
2026-08-21 17:52:45 +03:00
const loadInitData = ( async function () {
return fetch ( host + '/api/promo/get_initial_data' , { credentials : 'include' })
. then ( async response => {
const data = await response . json ();
2026-08-22 21:51:25 +03:00
if ( response . status === 400 ) {
return requestWithRefresh ( host + '/api/guest/clear_cookie' , { method : 'POST' })
. then ( r => { document . location . reload ()});
}
2026-08-21 17:52:45 +03:00
if ( response . status === 401 && data . error_message !== 'need_refresh' ) {
2026-08-22 21:51:25 +03:00
await initToken ();
await new Promise ( resolve => setTimeout (() => resolve (), 100 ));
return fetch ( host + '/api/promo/get_initial_data' , { credentials : 'include' })
. then ( r => r . json ());
2026-08-21 17:52:45 +03:00
}
if ( response . status === 403 || data . error_message === 'need_refresh' ) {
return await refreshToken ()
. then (( r ) => {
return fetch ( host + '/api/promo/get_initial_data' , { credentials : 'include' })
. then ( r => r . json ());
});
}
return data ;
})
. then (( data ) => {
updateCart ();
return data ;
})
. catch ( error => {
2026-08-22 21:51:25 +03:00
// return Promise.resolve(mockData);
return Promise . resolve ({
cart : [],
users : [],
initial_groups : [],
available_groups : [],
days : 7 ,
});
2026-08-21 17:52:45 +03:00
})
});
2026-08-22 21:51:25 +03:00
/**
* Дебаунс с группировкой идентификаторов и очередью.
*
* @param {Function} func - функция, принимающая массив ID и возвращающая Promise
* @param {number} delay - задержка в мс между последним кликом и отправкой запроса
* @returns {Function} - дебаунсированная функция, принимающая один ID и возвращающая Promise
*/
function debounceBatch ( func , delay ) {
let timerId = null ;
let pendingBatch = null ; // ожидает таймера
let runningBatch = null ; // выполняется
let nextBatch = null ; // накапливается во время выполнения
function processBatch ( batch ) {
runningBatch = batch ;
const { ids , promises } = batch ;
let result ;
try {
result = func ( ids );
} catch ( err ) {
promises . forEach ( p => p . reject ( err ));
finishBatch ();
return ;
}
if ( result && typeof result . then === 'function' ) {
result
. then ( data => promises . forEach ( p => p . resolve ( data )))
. catch ( err => promises . forEach ( p => p . reject ( err )))
. finally ( finishBatch );
} else {
promises . forEach ( p => p . resolve ( result ));
finishBatch ();
}
}
function finishBatch () {
runningBatch = null ;
// Если есть накопленный следующий батч – переносим в ожидающие и запускаем таймер
if ( nextBatch && nextBatch . ids . length > 0 ) {
pendingBatch = nextBatch ;
nextBatch = null ;
timerId = setTimeout (() => {
timerId = null ;
const batch = pendingBatch ;
pendingBatch = null ;
processBatch ( batch );
}, delay );
}
}
return function debounced ( id ) {
return new Promise (( resolve , reject ) => {
if ( runningBatch ) {
// Во время выполнения – добавляем в nextBatch
if ( ! nextBatch ) {
nextBatch = { ids : [], promises : [] };
}
nextBatch . ids . push ( id );
nextBatch . promises . push ({ resolve , reject });
} else if ( pendingBatch ) {
// Ожидает таймера – добавляем и перезапускаем таймер
pendingBatch . ids . push ( id );
pendingBatch . promises . push ({ resolve , reject });
clearTimeout ( timerId );
timerId = setTimeout (() => {
timerId = null ;
const batch = pendingBatch ;
pendingBatch = null ;
processBatch ( batch );
}, delay );
} else {
// Ничего нет – создаём новый ожидающий батч
pendingBatch = { ids : [ id ], promises : [{ resolve , reject }] };
timerId = setTimeout (() => {
timerId = null ;
const batch = pendingBatch ;
pendingBatch = null ;
processBatch ( batch );
}, delay );
}
});
};
}
2026-08-21 17:52:45 +03:00
function debounce ( func , delay ) {
let timeoutId ;
2026-08-22 21:51:25 +03:00
let rejectPrevious ; // функция для отклонения предыдущего промиса
2026-08-21 17:52:45 +03:00
return function (... args ) {
2026-08-22 21:51:25 +03:00
// Если есть ожидающий промис, отклоняем его
if ( rejectPrevious ) {
rejectPrevious ( new Error ( 'Debounced function cancelled' ));
rejectPrevious = null ;
}
// Создаём новый промис для текущего вызова
return new Promise (( resolve , reject ) => {
// Сохраняем reject, чтобы можно было отклонить этот промис позже
rejectPrevious = reject ;
// Очищаем старый таймер и устанавливаем новый
clearTimeout ( timeoutId );
timeoutId = setTimeout (() => {
try {
// Вызываем оригинальную функцию с переданными аргументами и контекстом
const result = func . apply ( this , args );
// Если результат — промис, дожидаемся его
if ( result && typeof result . then === 'function' ) {
result . then ( resolve ). catch ( reject );
} else {
resolve ( result );
}
} catch ( error ) {
reject ( error );
} finally {
// Сбрасываем, чтобы не отклонить случайно после выполнения
rejectPrevious = null ;
}
}, delay );
});
2026-08-21 17:52:45 +03:00
};
}
let allProducts ;
let visibleProducts ;
function renderProduct ( item , index ) {
const li = document . createElement ( 'li' );
li . className = 'cart-item' ;
const amount = item . unit === 'g' ? item . n + ' г ' : item . n + ' шт' ;
2026-08-21 18:23:20 +03:00
li . innerHTML = cartPolicy . createHTML ( `
2026-08-21 17:52:45 +03:00
<div class="item-image"><img src=" ${ item . image } " alt=""></div>
<div class="item-details">
<div class="item-name"> ${ item . name } </div>
<div class="item-price"> ${ amount } × ${ Math . round ( item . price * 100 ) / 100 } ₽</div>
</div>
<div class="menu-btn-wrapper">
<button class="menu-btn" data-index=" ${ index } ">⋮</button>
<div class="menu-dropdown">
<ul>
<li class="menu-replace">Заменить</li>
<li class="menu-remove">Удалить</li>
</ul>
</div>
</div>
2026-08-21 18:23:20 +03:00
` );
2026-08-21 17:52:45 +03:00
return li ;
}
let cartList ;
let scrollWrapper ;
let showMoreBtn ;
let remainingSpan ;
let initialData_g ;
2026-08-28 09:41:34 +03:00
const cartPolicy = window . trustedTypes . createPolicy ( 'cartPolicy' , {
2026-08-21 18:23:20 +03:00
createHTML : ( string ) => string
});
2026-08-21 17:52:45 +03:00
function renderProducts () {
2026-08-24 06:45:21 +03:00
const existingNodesByKey = new Map ();
Array . from ( cartList . children ). forEach ( node => {
existingNodesByKey . set ( node . dataset . productKey , node );
});
2026-08-21 17:52:45 +03:00
visibleProducts . forEach (( item , index ) => {
2026-08-24 06:45:21 +03:00
const key = item . name ;
const signature = JSON . stringify ({
name : item . name ,
n : item . n ,
price : item . price ,
total_price : item . total_price ,
currency : item . currency ,
image : item . image ,
unit : item . unit ,
});
let node = existingNodesByKey . get ( key );
if ( ! node || node . dataset . productSignature !== signature ) {
const oldNode = node ;
node = renderProduct ( item , index );
node . dataset . productKey = key ;
node . dataset . productSignature = signature ;
node . getElementsByClassName ( 'item-image' )[ 0 ]. addEventListener ( 'click' , () => openModal (
item . image . replace ( '65,fit' , '335,fit' ),
item . name ,
item . n ,
item . total_price ,
() => { console . log ( 'replace' ) },
() => { console . log ( 'another' ) },
));
if ( oldNode ) {
oldNode . replaceWith ( node );
}
} else {
const menuBtn = node . querySelector ( '.menu-btn' );
if ( menuBtn ) {
menuBtn . dataset . index = index ;
}
}
const currentNodeAtPosition = cartList . children [ index ];
if ( currentNodeAtPosition !== node ) {
cartList . insertBefore ( node , currentNodeAtPosition || null );
}
existingNodesByKey . delete ( key );
});
existingNodesByKey . forEach ( node => {
node . remove ();
2026-08-21 17:52:45 +03:00
});
const remaining = allProducts . length - visibleProducts . length ;
if ( remaining > 0 ) {
showMoreBtn . classList . remove ( 'hidden' );
remainingSpan . textContent = remaining ;
} else {
showMoreBtn . classList . add ( 'hidden' );
}
2026-08-24 06:45:21 +03:00
2026-08-21 17:52:45 +03:00
updateSummaryCount ();
calculateListHeight ();
}
function updateSummaryCount () {
const totalPrice = new Intl . NumberFormat ( "ru-RU" , { style : "currency" , currency : "RUB" }). format (
Math . round ( allProducts . reduce (( sum , item ) => sum + item . total_price , 0 ) * 100 ) / 100 ,
);
const x = Math . round ( initialData_g . days * initialData_g . excess + initialData_g . days );
if ( allProducts . length === 0 ) {
2026-08-22 21:51:25 +03:00
document . getElementById ( 'summary-details' ). innerHTML = cartPolicy . createHTML ( `` );
document . getElementById ( 'empty-cart-loader' ). classList . remove ( 'hidden' );
} else {
document . getElementById ( 'empty-cart-loader' ). classList . add ( 'hidden' );
document . getElementById ( 'summary-details' ). innerHTML = cartPolicy . createHTML ( `<span> ${ allProducts . length } ${ declension ( allProducts . length , [ 'товар' , 'товара' , 'товаров' ]) } </span> <b>•</b> <span> ${ totalPrice } </span> <div class="summary-excess">С запасами на ${ x } ${ declension ( x , [ 'день' , 'дня' , 'дней' ]) } </div>` );
document . getElementById ( 'total-price' ). innerHTML = cartPolicy . createHTML ( ` ${ totalPrice } ` );
2026-08-21 17:52:45 +03:00
}
}
function calculateListHeight () {
const leftCol = document . querySelector ( '.col-left' );
if ( ! leftCol ) return ;
const title = leftCol . querySelector ( 'h1' );
const subtitle = leftCol . querySelector ( '.subtitle' );
const summary = leftCol . querySelector ( '.cart-summary' );
const footer = leftCol . querySelector ( '.cart-footer' );
const showMore = leftCol . querySelector ( '.show-more' );
let fixedHeight = 0 ;
if ( title ) fixedHeight += title . offsetHeight + 6 ;
if ( subtitle ) fixedHeight += subtitle . offsetHeight + 28 ;
if ( summary ) fixedHeight += summary . offsetHeight + 24 ;
if ( showMore && ! showMore . classList . contains ( 'hidden' )) fixedHeight += showMore . offsetHeight ;
if ( footer ) fixedHeight += footer . offsetHeight + 20 ;
const paddingY = 60 ;
const rect = leftCol . getBoundingClientRect ();
const availHeight = window . innerHeight - rect . top - paddingY - 20 ;
const listHeight = availHeight - fixedHeight ;
if ( listHeight > 100 ) {
scrollWrapper . style . maxHeight = ` ${ listHeight } px` ;
scrollWrapper . style . overflowY = 'auto' ;
}
}
function declension ( number , titles ) {
const cases = [ 2 , 0 , 1 , 1 , 1 , 2 ];
const idx = ( number % 100 > 4 && number % 100 < 20 )
? 2
: cases [ Math . min ( number % 10 , 5 )];
return titles [ idx ];
}
function wholeRender ( initialData ) {
initialData_g = initialData ;
document . querySelector ( '.summary-days' ). textContent = ` ${ initialData_g . days } ${ declension ( initialData_g . days , [ 'день' , 'дня' , 'дней' ]) } ` ;
allProducts = ( Array . isArray ( initialData_g . cart ) ? initialData_g . cart : [] ?? []). flat (). map ( item => {
return {... item , image : item . image ? . replace ( '{SIZE}' , '65,fit' )}
});
visibleProducts = allProducts . slice ( 0 , visibleProducts ? visibleProducts . length : 5 );
document . querySelectorAll ( '.days-options .day-btn' ). forEach ( btn => {
if ( btn . innerText . trim () === ` ${ initialData_g . days } ` ) {
btn . classList . add ( 'active' );
}
});
// Массив людей теперь содержит поля запрещенных и любимых продуктов
console . log ( initialData_g );
let people = initialData_g . users ;
// --- 1. Рендеринг списка продуктов (Левая часть) ---
cartList = document . getElementById ( 'cart-list' );
scrollWrapper = document . getElementById ( 'scroll-wrapper' );
showMoreBtn = document . getElementById ( 'show-more-btn' );
remainingSpan = document . getElementById ( 'remaining-count' );
renderProducts ();
// --- 2. Меню (Заменить и Удалить) ---
document . addEventListener ( 'click' , function ( e ) {
const menuBtn = e . target . closest ( '.menu-btn' );
const menuDropdown = e . target . closest ( '.menu-dropdown' );
const isReplace = e . target . closest ( '.menu-replace' );
const isRemove = e . target . closest ( '.menu-remove' );
const isClearAll = e . target . closest ( '.reorder-btn' );
2026-08-22 21:51:25 +03:00
const isCloseModal = e . target . closest ( '.product-modal-overlay' );
2026-08-21 17:52:45 +03:00
if ( menuBtn ) {
const dropdown = menuBtn . nextElementSibling ;
const isOpen = dropdown . classList . contains ( 'active' );
document . querySelectorAll ( '.menu-dropdown.active' ). forEach ( el => el . classList . remove ( 'active' ));
if ( ! isOpen ) {
dropdown . classList . add ( 'active' );
}
e . stopPropagation ();
} else if ( ! menuDropdown ) {
document . querySelectorAll ( '.menu-dropdown.active' ). forEach ( el => el . classList . remove ( 'active' ));
}
if ( isReplace ) {
const wrapper = isReplace . closest ( '.menu-btn-wrapper' );
const li = wrapper . closest ( '.cart-item' );
li . classList . add ( 'replacing' );
const index = Array . from ( cartList . children ). indexOf ( li );
replaceProduct ( allProducts [ index ]. name ). then (() => {
li . remove ();
document . querySelectorAll ( '.menu-dropdown.active' ). forEach ( el => el . classList . remove ( 'active' ));
allProducts . splice ( index , 1 );
2026-08-24 06:45:21 +03:00
visibleProducts = allProducts . slice ( 0 , visibleProducts ? visibleProducts . length : 5 );
2026-08-21 17:52:45 +03:00
renderProducts ();
calculateListHeight ();
});
}
if ( isRemove ) {
const wrapper = isRemove . closest ( '.menu-btn-wrapper' );
const li = wrapper . closest ( '.cart-item' );
li . classList . add ( 'replacing' );
const index = Array . from ( cartList . children ). indexOf ( li );
deleteProduct ( allProducts [ index ]. name ). then (() => {
li . remove ();
document . querySelectorAll ( '.menu-dropdown.active' ). forEach ( el => el . classList . remove ( 'active' ));
visibleProducts . splice ( index , 1 );
allProducts . splice ( index , 1 );
if ( visibleProducts . length < allProducts . length ) {
visibleProducts . push ( allProducts [ visibleProducts . length ]);
}
renderProducts ();
calculateListHeight ();
})
}
if ( isClearAll ) {
requestWithRefresh ( host + '/api/guest/clear_cookie' , { method : 'POST' })
. then ( r => { document . location . reload ()});
}
2026-08-22 21:51:25 +03:00
if ( isCloseModal ) {
document . getElementById ( 'modalProductOverlay' ). classList . remove ( 'show' );
}
2026-08-21 17:52:45 +03:00
});
showMoreBtn . addEventListener ( 'click' , () => {
while ( visibleProducts . length < allProducts . length ) {
visibleProducts . push ( allProducts [ visibleProducts . length ]);
}
renderProducts ();
scrollWrapper . style . maxHeight = '500px' ;
calculateListHeight ();
});
// --- 3. Фиксированная высота ---
window . addEventListener ( 'resize' , calculateListHeight );
calculateListHeight ();
// --- 4. Аккордеон (активна только одна вкладка) ---
document . querySelectorAll ( '.accordion-header' ). forEach ( header => {
header . addEventListener ( 'click' , function () {
const item = this . closest ( '.accordion-item' );
const isOpen = item . classList . contains ( 'open' );
document . querySelectorAll ( '.accordion-item' ). forEach ( el => el . classList . remove ( 'open' ));
if ( ! isOpen ) {
item . classList . add ( 'open' );
}
});
});
// --- 5. Дни ---
document . querySelectorAll ( '.day-btn' ). forEach ( btn => {
btn . addEventListener ( 'click' , function () {
const parent = this . closest ( '.days-options' );
parent . querySelectorAll ( '.day-btn' ). forEach ( b => b . classList . remove ( 'active' ));
this . classList . add ( 'active' );
setOption ( 'days' , this . dataset . days );
document . querySelector ( '.summary-days' ). textContent = ` ${ this . dataset . days } ${ declension ( this . dataset . days , [ 'день' , 'дня' , 'дней' ]) } ` ;
});
});
// --- 6. Семья с возможностью редактирования ---
const familyList = document . getElementById ( 'family-list' );
const familyDesc = document . getElementById ( 'family-desc' );
let editingIndex = null ;
function renderPeople () {
2026-08-21 18:23:20 +03:00
familyList . innerHTML = cartPolicy . createHTML ( '' );
2026-08-21 17:52:45 +03:00
people . forEach (( person , index ) => {
2026-08-22 21:51:25 +03:00
let avatar ;
if ( person . age < 16 ) {
avatar = '<img src="/images/broc-' + ( person . gender === 'f' ? 'girl' : 'boy' ) + '.png" alt="ребёнок" />' ;
} else {
avatar = person . gender === 'f' ? '<img src="/images/broc-woman.png" alt="девушка" />' : '<img src="/images/broc-man.png" alt="парень" />' ;
}
2026-08-21 17:52:45 +03:00
const div = document . createElement ( 'div' );
div . className = 'family-member' ;
div . dataset . index = index ;
2026-08-21 18:23:20 +03:00
div . innerHTML = cartPolicy . createHTML ( `
2026-08-21 17:52:45 +03:00
<div class="avatar"> ${ avatar } </div>
<div class="info">
<div class="name"> ${ person . name } </div>
<div class="age"> ${ person . age } лет</div>
</div>
<div class="delete-member" data-index=" ${ index } ">× </div>
2026-08-21 18:23:20 +03:00
` );
2026-08-21 17:52:45 +03:00
familyList . appendChild ( div );
});
const count = people . length ;
familyDesc . textContent = ` ${ count } ${ declension ( count , [ 'человек' , 'человека' , 'человек' ]) } ` ;
if ( count === 0 ) familyDesc . textContent = 'Не указаны' ;
document . querySelectorAll ( '.delete-member' ). forEach ( btn => {
btn . addEventListener ( 'click' , function ( e ) {
e . stopPropagation ();
const idx = parseInt ( this . dataset . index );
people . splice ( idx , 1 );
renderPeople ();
setOption ( 'users' , people );
});
});
}
renderPeople ();
familyList . addEventListener ( 'click' , function ( e ) {
const member = e . target . closest ( '.family-member' );
if ( ! member ) return ;
if ( e . target . closest ( '.delete-member' )) return ;
const idx = parseInt ( member . dataset . index );
openEditModal ( idx );
});
// --- 7. Попап (Добавление и Редактирование) + Логика Тегов ---
const modal = document . getElementById ( 'personModal' );
const addBtn = document . getElementById ( 'addMemberBtn' );
const closeBtn = document . getElementById ( 'closeModalBtn' );
const saveBtn = document . getElementById ( 'savePersonBtn' );
const modalTitle = document . getElementById ( 'modalTitle' );
const genderBtns = document . querySelectorAll ( '.gender-btn' );
const availableTags = initialData_g . available_groups ;
function openEditModal ( index = null ) {
editingIndex = index ;
// Очистка тегов внутри попапа
2026-08-21 18:23:20 +03:00
document . querySelectorAll ( '#personModal .tags-wrap' ). forEach ( el => el . innerHTML = cartPolicy . createHTML ( '' ));
2026-08-21 17:52:45 +03:00
if ( editingIndex !== null ) {
modalTitle . textContent = 'Редактировать человека' ;
const p = people [ editingIndex ];
document . getElementById ( 'inputName' ). value = p . name ;
document . getElementById ( 'inputAge' ). value = p . age ;
document . getElementById ( 'inputWeight' ). value = p . weight ;
document . getElementById ( 'inputHeight' ). value = p . height ;
genderBtns . forEach ( b => {
b . classList . toggle ( 'active' , b . dataset . gender === p . gender );
});
// Рендерим сохраненные теги
if ( p . avoid ) p . avoid . forEach ( tag => addTagToModal ( tag , 'avoid' , false ));
if ( p . favorite ) p . favorite . forEach ( tag => addTagToModal ( tag , 'favorite' , false ));
} else {
modalTitle . textContent = 'Добавить человека' ;
document . getElementById ( 'inputName' ). value = '' ;
document . getElementById ( 'inputAge' ). value = '' ;
document . getElementById ( 'inputWeight' ). value = '' ;
document . getElementById ( 'inputHeight' ). value = '' ;
genderBtns . forEach ( b => b . classList . remove ( 'active' ));
document . querySelector ( '.gender-btn[data-gender="m"]' ). classList . add ( 'active' );
}
modal . style . display = 'flex' ;
calculateListHeight ();
}
addBtn . addEventListener ( 'click' , () => openEditModal ( null ));
function closeModal () { modal . style . display = 'none' ; }
closeBtn . addEventListener ( 'click' , closeModal );
modal . addEventListener ( 'click' , function ( e ) {
if ( e . target === this ) closeModal ();
});
genderBtns . forEach ( btn => {
btn . addEventListener ( 'click' , function () {
genderBtns . forEach ( b => b . classList . remove ( 'active' ));
this . classList . add ( 'active' );
});
});
// Логика добавления тега в попап
function addTagToModal ( text , type , saveToPeople = true ) {
const tagsWrap = document . querySelector ( `#personModal .tags-wrap[data-target=" ${ type } "]` );
// Проверка дубликата в своей группе
const existingInGroup = tagsWrap . querySelectorAll ( '.tag' );
for ( let t of existingInGroup ) {
if ( t . textContent . trim (). replace ( '× ' , '' ). trim () === text ) return ;
}
// Взаимоисключение: если тег есть в противоположной группе - удалить его оттуда
const oppositeType = type === 'avoid' ? 'favorite' : 'avoid' ;
const oppositeWrap = document . querySelector ( `#personModal .tags-wrap[data-target=" ${ oppositeType } "]` );
const oppositeTags = oppositeWrap . querySelectorAll ( '.tag' );
for ( let t of oppositeTags ) {
if ( t . textContent . trim (). replace ( '× ' , '' ). trim () === text ) {
t . remove ();
break ;
}
}
const tag = document . createElement ( 'div' );
tag . className = `tag ${ type === 'avoid' ? 'tag-red' : 'tag-green' } ` ;
2026-08-21 18:23:20 +03:00
tag . innerHTML = cartPolicy . createHTML ( ` ${ text } <span class="tag-close">× </span>` );
2026-08-21 17:52:45 +03:00
tag . querySelector ( '.tag-close' ). addEventListener ( 'click' , function () {
tag . remove ();
});
tagsWrap . appendChild ( tag );
}
// Автокомплит для полей в попапе
document . querySelectorAll ( '#personModal .pref-input' ). forEach ( input => {
const type = input . dataset . type ;
const listEl = document . querySelector ( `#personModal .autocomplete-list[data-type=" ${ type } "]` );
const tagsWrap = document . querySelector ( `#personModal .tags-wrap[data-target=" ${ type } "]` );
input . addEventListener ( 'input' , function () {
const val = this . value . toLowerCase ();
2026-08-21 18:23:20 +03:00
listEl . innerHTML = cartPolicy . createHTML ( '' );
2026-08-21 17:52:45 +03:00
// Ищем все уже выбранные теги в обеих группах внутри попапа
const allSelectedTags = new Set ();
document . querySelectorAll ( '#personModal .tags-wrap .tag' ). forEach ( tagEl => {
const tagText = tagEl . textContent . trim (). replace ( '× ' , '' ). trim ();
if ( tagText ) allSelectedTags . add ( tagText );
});
if ( val . length > 0 ) {
const matches = availableTags . filter ( tag =>
tag . toLowerCase (). includes ( val ) && ! allSelectedTags . has ( tag )
);
if ( matches . length > 0 ) {
listEl . classList . add ( 'active' );
matches . forEach ( match => {
const div = document . createElement ( 'div' );
div . textContent = match ;
div . addEventListener ( 'click' , function () {
addTagToModal ( match , type );
input . value = '' ;
listEl . classList . remove ( 'active' );
});
listEl . appendChild ( div );
});
} else {
listEl . classList . remove ( 'active' );
}
} else {
listEl . classList . remove ( 'active' );
}
});
document . addEventListener ( 'click' , function ( e ) {
if ( ! input . contains ( e . target ) && ! listEl . contains ( e . target )) {
listEl . classList . remove ( 'active' );
}
});
});
// Сохранение пользователя (сбор тегов из DOM попапа)
saveBtn . addEventListener ( 'click' , () => {
const name = document . getElementById ( 'inputName' ). value . trim ();
const age = document . getElementById ( 'inputAge' ). value . trim ();
const weight = document . getElementById ( 'inputWeight' ). value . trim ();
const height = document . getElementById ( 'inputHeight' ). value . trim ();
const gender = document . querySelector ( '.gender-btn.active' ). dataset . gender ;
if ( ! name ) { alert ( 'Пожалуйста, введите имя.' ); return ; }
if ( ! age || parseInt ( age ) < 7 ) { alert ( 'Возраст должен быть от 7 лет.' ); return ; }
// Сбор тегов
const forbTags = [];
document . querySelector ( '#personModal .tags-wrap[data-target="avoid"]' ). querySelectorAll ( '.tag' ). forEach ( el => {
forbTags . push ( el . textContent . trim (). replace ( '× ' , '' ). trim ());
});
const favTags = [];
document . querySelector ( '#personModal .tags-wrap[data-target="favorite"]' ). querySelectorAll ( '.tag' ). forEach ( el => {
favTags . push ( el . textContent . trim (). replace ( '× ' , '' ). trim ());
});
2026-09-06 01:56:59 +03:00
const personData = { name , age , weight , height , gender , avoid : forbTags , favorite : favTags };
2026-08-21 17:52:45 +03:00
if ( editingIndex !== null ) {
people [ editingIndex ] = personData ;
editingIndex = null ;
} else {
people . push ( personData );
}
renderPeople ();
setOption ( 'users' , people );
closeModal ();
});
// --- 8. Блок: Категории ---
const categories = initialData_g . available_groups ;
const initialSelected = initialData_g . initial_groups ;
let selectedCategories = [... initialSelected ];
2026-08-22 21:51:25 +03:00
// let selectedCategories = [];
2026-08-21 17:52:45 +03:00
let isCategoriesExpanded = false ;
2026-08-22 21:51:25 +03:00
const maxVisibleCategories = 7 ;
const sendUpdatedCategories = debounceBatch (( categoriesNames ) => {
let localCategories = [... selectedCategories ]
categoriesNames . forEach (( categoryName ) => {
if ( localCategories . includes ( categoryName )) {
localCategories . splice ( localCategories . indexOf ( categoryName ), 1 );
} else {
localCategories . push ( categoryName );
}
})
console . log ( 'send' , localCategories );
return setOption ( 'categories' , localCategories )
2026-08-21 17:52:45 +03:00
}, 1500 );
2026-08-22 21:51:25 +03:00
2026-08-21 17:52:45 +03:00
const categoriesGrid = document . getElementById ( 'categories-grid' );
2026-08-22 21:51:25 +03:00
const prefsAccordionBlock = document . getElementById ( 'prefs-accordion' );
const prefsDescription = document . getElementById ( 'prefs-desc' );
2026-08-21 17:52:45 +03:00
function renderCategories () {
2026-08-21 18:23:20 +03:00
categoriesGrid . innerHTML = cartPolicy . createHTML ( '' );
2026-08-22 21:51:25 +03:00
if ( availableTags . length > 0 ) {
prefsAccordionBlock . classList . add ( 'visible' );
} else {
prefsAccordionBlock . classList . remove ( 'visible' );
}
if ( selectedCategories . length > 0 ) {
document . getElementById ( 'preferences-icon' ). classList . add ( 'bg-green-light' );
document . getElementById ( 'preferences-icon' ). classList . remove ( 'bg-gray-light' );
if ( selectedCategories . length === 1 ) {
prefsDescription . innerText = 'Выбрана 1 категория' ;
} else {
prefsDescription . innerText = 'Выбраны ' + selectedCategories . length + ' ' + declension ( selectedCategories . length , [ 'категория' , 'категории' , 'категорий' ]);
}
} else {
document . getElementById ( 'preferences-icon' ). classList . add ( 'bg-gray-light' );
document . getElementById ( 'preferences-icon' ). classList . remove ( 'bg-green-light' );
prefsDescription . innerText = 'Не указаны' ;
}
const dataToShow = isCategoriesExpanded ? categories : (
( selectedCategories . length > 0 ? selectedCategories : categories ). slice ( 0 , maxVisibleCategories )
);
2026-08-21 17:52:45 +03:00
dataToShow . forEach ( cat => {
const btn = document . createElement ( 'button' );
btn . className = `category-btn ${ selectedCategories . includes ( cat ) ? 'active' : 'inactive' } ` ;
2026-08-22 21:51:25 +03:00
btn . innerHTML = cartPolicy . createHTML ( `<span class="spacer-over"> ${ cat } </span><span class="spacer-width"> ${ cat } </span>` );
2026-08-21 17:52:45 +03:00
btn . dataset . category = cat ;
btn . addEventListener ( 'click' , function () {
2026-08-22 21:51:25 +03:00
if ( btn . classList . contains ( 'loading-border' )) {
return ;
2026-08-21 17:52:45 +03:00
}
2026-08-22 21:51:25 +03:00
const catName = this . dataset . category ;
btn . classList . add ( 'loading-border' );
sendUpdatedCategories ( catName )
. finally (() => {
btn . classList . remove ( 'loading-border' );
});
2026-08-21 17:52:45 +03:00
});
categoriesGrid . appendChild ( btn );
});
2026-08-22 21:51:25 +03:00
if ( dataToShow . length > 0 ) {
const toggleBtn = document . createElement ( 'button' );
toggleBtn . className = 'toggle-categories-btn'
toggleBtn . id = 'expand-categories'
toggleBtn . innerHTML = cartPolicy . createHTML (
( ! isCategoriesExpanded && dataToShow . length < selectedCategories . length ? '+' + ( selectedCategories . length - dataToShow . length ) : '' ) +
' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"\n' +
' stroke-linejoin="round">\n' +
' <polyline points="18 15 12 9 6 15"></polyline>\n' +
' </svg>' );
if ( isCategoriesExpanded ) {
toggleBtn . classList . remove ( 'collapsed' );
toggleBtn . classList . remove ( 'show-more' );
} else {
toggleBtn . classList . add ( 'collapsed' );
if ( dataToShow . length < selectedCategories . length ) {
toggleBtn . classList . add ( 'show-more' );
}
}
toggleBtn . addEventListener ( 'click' , function () {
isCategoriesExpanded = ! isCategoriesExpanded ;
renderCategories ();
});
categoriesGrid . appendChild ( toggleBtn );
2026-08-21 17:52:45 +03:00
}
}
2026-08-22 21:51:25 +03:00
2026-08-21 17:52:45 +03:00
renderCategories ();
}
document . addEventListener ( 'DOMContentLoaded' , async () => {
wholeRender ( await loadInitData ());
});
document . addEventListener ( 'DOMContentLoaded' , function () {
const popup = document . getElementById ( 'cookie-popup' );
const acceptBtn = document . getElementById ( 'cookie-accept' );
// Проверяем, согласился ли пользователь ранее
if ( ! localStorage . getItem ( 'cookie_accepted' )) {
// Небольшая задержка для красоты появления
setTimeout (() => {
popup . classList . add ( 'show' );
}, 500 );
}
// Обработчик нажатия на кнопку "Хорошо"
acceptBtn . addEventListener ( 'click' , function () {
// Сохраняем согласие в браузер
localStorage . setItem ( 'cookie_accepted' , 'true' );
// Запускаем анимацию исчезновения
popup . classList . remove ( 'show' );
popup . classList . add ( 'hide' );
// Полностью удаляем из DOM после завершения анимации
setTimeout (() => {
popup . style . display = 'none' ;
}, 400 );
});
2026-08-22 21:51:25 +03:00
document . getElementById ( 'modalProductOverlay' ). addEventListener ( 'click' , function ( e ) {
if ( e . target === this ) {
this . classList . remove ( 'active' );
}
});
});
function openModal ( imageSrc , name , quantity , price , onReplace , onAnother ) {
// Получаем элементы по новым id
const overlay = document . getElementById ( 'modalProductOverlay' );
const img = document . getElementById ( 'modalProductImage' );
const title = document . getElementById ( 'modalProductTitle' );
const qtySpan = document . getElementById ( 'modalProductQuantity' );
const priceSpan = document . getElementById ( 'modalProductPrice' );
const replaceBtn = document . getElementById ( 'modalProductReplaceBtn' );
const anotherBtn = document . getElementById ( 'modalProductAnotherBtn' );
// Заполняем данными
img . src = imageSrc || '' ;
img . alt = name || 'Товар' ;
title . textContent = name || 'Без названия' ;
qtySpan . textContent = quantity !== undefined ? quantity : '—' ;
priceSpan . textContent = new Intl . NumberFormat ( "ru-RU" , { style : "currency" , currency : "RUB" }). format (
Math . round ( price * 100 ) / 100 ,
);
// Удаляем старые обработчики, если они были (храним в свойствах кнопок)
if ( replaceBtn . _listener ) {
replaceBtn . removeEventListener ( 'click' , replaceBtn . _listener );
delete replaceBtn . _listener ;
}
if ( anotherBtn . _listener ) {
anotherBtn . removeEventListener ( 'click' , anotherBtn . _listener );
delete anotherBtn . _listener ;
}
// Создаём новые обработчики, которые вызывают переданные колбэки
const replaceHandler = function () {
if ( typeof onReplace === 'function' ) onReplace ();
};
const anotherHandler = function () {
if ( typeof onAnother === 'function' ) onAnother ();
};
// Сохраняем ссылки и добавляем слушатели
replaceBtn . _listener = replaceHandler ;
anotherBtn . _listener = anotherHandler ;
replaceBtn . addEventListener ( 'click' , replaceHandler );
anotherBtn . addEventListener ( 'click' , anotherHandler );
// Показываем модалку
overlay . classList . add ( 'show' );
}