2026-08-22 18:51:25 +00:00
"use strict" ;
2026-08-21 15:23:20 +00:00
const host = 'https://local.hvatilo.ru:8055' ;
2026-08-21 14:52:45 +00: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 18:51:25 +00: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 14:52:45 +00: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 18:51:25 +00:00
document . getElementById ( 'summary-loader' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'summary-details' ) . classList . add ( 'hidden' ) ;
2026-08-21 14:52:45 +00: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 18:51:25 +00: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 14:52:45 +00: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 18:51:25 +00: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 14:52:45 +00:00
} )
}
2026-08-22 18:51:25 +00: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 14:52:45 +00: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 18:51:25 +00:00
if ( response . status === 400 ) {
return requestWithRefresh ( host + '/api/guest/clear_cookie' , { method : 'POST' } )
. then ( r => { document . location . reload ( ) } ) ;
}
2026-08-21 14:52:45 +00:00
if ( response . status === 401 && data . error _message !== 'need_refresh' ) {
2026-08-22 18:51:25 +00: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 14:52:45 +00: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 18:51:25 +00:00
// return Promise.resolve(mockData);
return Promise . resolve ( {
cart : [ ] ,
users : [ ] ,
initial _groups : [ ] ,
available _groups : [ ] ,
days : 7 ,
} ) ;
2026-08-21 14:52:45 +00:00
} )
} ) ;
2026-08-22 18:51:25 +00: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 14:52:45 +00:00
function debounce ( func , delay ) {
let timeoutId ;
2026-08-22 18:51:25 +00:00
let rejectPrevious ; // функция для отклонения предыдущего промиса
2026-08-21 14:52:45 +00:00
return function ( ... args ) {
2026-08-22 18:51:25 +00: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 14:52:45 +00: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 15:23:20 +00:00
li . innerHTML = cartPolicy . createHTML ( `
2026-08-21 14:52:45 +00:00
< div class = "item-image" > < img src = "${item.image}" alt = "" > < / d i v >
< div class = "item-details" >
< div class = "item-name" > $ { item . name } < / d i v >
< div class = "item-price" > $ { amount } & times ; $ { Math . round ( item . price * 100 ) / 100 } ₽ < / d i v >
< / d i v >
< div class = "menu-btn-wrapper" >
< button class = "menu-btn" data - index = "${index}" > & # 8942 ; < / b u t t o n >
< div class = "menu-dropdown" >
< ul >
< li class = "menu-replace" > Заменить < / l i >
< li class = "menu-remove" > Удалить < / l i >
< / u l >
< / d i v >
< / d i v >
2026-08-21 15:23:20 +00:00
` );
2026-08-21 14:52:45 +00:00
return li ;
}
let cartList ;
let scrollWrapper ;
let showMoreBtn ;
let remainingSpan ;
let initialData _g ;
2026-08-21 15:23:20 +00:00
const cartPolicy = trustedTypes . createPolicy ( 'cartPolicy' , {
createHTML : ( string ) => string
} ) ;
2026-08-21 14:52:45 +00:00
function renderProducts ( ) {
2026-08-21 15:23:20 +00:00
cartList . innerHTML = cartPolicy . createHTML ( '' ) ;
2026-08-21 14:52:45 +00:00
visibleProducts . forEach ( ( item , index ) => {
2026-08-22 18:51:25 +00:00
let node = renderProduct ( item , index )
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' ) } ,
) ) ;
cartList . appendChild ( node ) ;
2026-08-21 14:52:45 +00:00
} ) ;
const remaining = allProducts . length - visibleProducts . length ;
if ( remaining > 0 ) {
showMoreBtn . classList . remove ( 'hidden' ) ;
remainingSpan . textContent = remaining ;
} else {
showMoreBtn . classList . add ( 'hidden' ) ;
}
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 18:51:25 +00: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 14:52:45 +00: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 18:51:25 +00:00
const isCloseModal = e . target . closest ( '.product-modal-overlay' ) ;
2026-08-21 14:52:45 +00: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' ) ) ;
visibleProducts . splice ( index , 1 ) ;
allProducts . splice ( index , 1 ) ;
if ( visibleProducts . length < allProducts . length ) {
visibleProducts . push ( allProducts [ visibleProducts . length ] ) ;
}
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 18:51:25 +00:00
if ( isCloseModal ) {
document . getElementById ( 'modalProductOverlay' ) . classList . remove ( 'show' ) ;
}
2026-08-21 14:52:45 +00: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 15:23:20 +00:00
familyList . innerHTML = cartPolicy . createHTML ( '' ) ;
2026-08-21 14:52:45 +00:00
people . forEach ( ( person , index ) => {
2026-08-22 18:51:25 +00: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 14:52:45 +00:00
const div = document . createElement ( 'div' ) ;
div . className = 'family-member' ;
div . dataset . index = index ;
2026-08-21 15:23:20 +00:00
div . innerHTML = cartPolicy . createHTML ( `
2026-08-21 14:52:45 +00:00
< div class = "avatar" > $ { avatar } < / d i v >
< div class = "info" >
< div class = "name" > $ { person . name } < / d i v >
< div class = "age" > $ { person . age } лет < / d i v >
< / d i v >
< div class = "delete-member" data - index = "${index}" > × < / d i v >
2026-08-21 15:23:20 +00:00
` );
2026-08-21 14:52:45 +00: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 15:23:20 +00:00
document . querySelectorAll ( '#personModal .tags-wrap' ) . forEach ( el => el . innerHTML = cartPolicy . createHTML ( '' ) ) ;
2026-08-21 14:52:45 +00: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 15:23:20 +00:00
tag . innerHTML = cartPolicy . createHTML ( ` ${ text } <span class="tag-close">× </span> ` ) ;
2026-08-21 14:52:45 +00: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 15:23:20 +00:00
listEl . innerHTML = cartPolicy . createHTML ( '' ) ;
2026-08-21 14:52:45 +00: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 ( ) ) ;
} ) ;
const personData = { name , age , weight , height , gender , avoid : forbTags , favorite : favTags } ;
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 18:51:25 +00:00
// let selectedCategories = [];
2026-08-21 14:52:45 +00:00
let isCategoriesExpanded = false ;
2026-08-22 18:51:25 +00: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 14:52:45 +00:00
} , 1500 ) ;
2026-08-22 18:51:25 +00:00
2026-08-21 14:52:45 +00:00
const categoriesGrid = document . getElementById ( 'categories-grid' ) ;
2026-08-22 18:51:25 +00:00
const prefsAccordionBlock = document . getElementById ( 'prefs-accordion' ) ;
const prefsDescription = document . getElementById ( 'prefs-desc' ) ;
2026-08-21 14:52:45 +00:00
function renderCategories ( ) {
2026-08-21 15:23:20 +00:00
categoriesGrid . innerHTML = cartPolicy . createHTML ( '' ) ;
2026-08-22 18:51:25 +00: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 14:52:45 +00:00
dataToShow . forEach ( cat => {
const btn = document . createElement ( 'button' ) ;
btn . className = ` category-btn ${ selectedCategories . includes ( cat ) ? 'active' : 'inactive' } ` ;
2026-08-22 18:51:25 +00:00
btn . innerHTML = cartPolicy . createHTML ( ` <span class="spacer-over"> ${ cat } </span><span class="spacer-width"> ${ cat } </span> ` ) ;
2026-08-21 14:52:45 +00:00
btn . dataset . category = cat ;
btn . addEventListener ( 'click' , function ( ) {
2026-08-22 18:51:25 +00:00
if ( btn . classList . contains ( 'loading-border' ) ) {
return ;
2026-08-21 14:52:45 +00:00
}
2026-08-22 18:51:25 +00:00
const catName = this . dataset . category ;
btn . classList . add ( 'loading-border' ) ;
sendUpdatedCategories ( catName )
. finally ( ( ) => {
btn . classList . remove ( 'loading-border' ) ;
} ) ;
2026-08-21 14:52:45 +00:00
} ) ;
categoriesGrid . appendChild ( btn ) ;
} ) ;
2026-08-22 18:51:25 +00: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 14:52:45 +00:00
}
}
2026-08-22 18:51:25 +00:00
2026-08-21 14:52:45 +00: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 18:51:25 +00: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' ) ;
}