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 ( ) {
return fetch ( host + '/api/guest/refresh_cookie' , { method : 'POST' , credentials : 'include' } ) . then ( response => {
return response . json ( ) ;
} )
}
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 ( ) {
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 ] ;
}
}
} )
}
console . log ( allProducts . length , visibleProducts . length ) ;
if ( d . status !== 'waiting' ) {
renderProducts ( ) ;
}
updateCart ( ) ;
}
} )
. 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 ( ) ;
} )
}
const loadInitData = ( async function ( ) {
return fetch ( host + '/api/promo/get_initial_data' , { credentials : 'include' } )
. then ( async response => {
const data = await response . json ( ) ;
if ( response . status === 401 && data . error _message !== 'need_refresh' ) {
return await initToken ( )
. then ( ( r ) => {
return fetch ( host + '/api/promo/get_initial_data' , { credentials : 'include' } )
. then ( r => r . json ( ) ) ;
} ) ;
}
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 => {
console . log ( error ) ;
return Promise . reject ( ) ;
} )
} ) ;
function debounce ( func , delay ) {
let timeoutId ;
return function ( ... args ) {
clearTimeout ( timeoutId ) ;
timeoutId = setTimeout ( ( ) => {
func . apply ( this , args ) ;
} , delay ) ;
} ;
}
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 ) => {
cartList . appendChild ( renderProduct ( item , index ) ) ;
} ) ;
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 ) ;
2026-08-21 15:23:20 +00:00
document . getElementById ( 'summary-details' ) . innerHTML = cartPolicy . createHTML ( ` ${ allProducts . length } ${ declension ( allProducts . length , [ 'товар' , 'товара' , 'товаров' ] ) } • ${ totalPrice } ~ с запасами на ${ x } ${ declension ( x , [ 'день' , 'дня' , 'дней' ] ) } , точность ${ Math . round ( initialData _g . precision * 100 ) / 100 } % ` ) ;
document . getElementById ( 'total-price' ) . innerHTML = cartPolicy . createHTML ( ` ${ totalPrice } ` ) ;
2026-08-21 14:52:45 +00:00
if ( allProducts . length === 0 ) {
2026-08-21 15:23:20 +00:00
document . getElementById ( 'summary-details' ) . innerHTML = cartPolicy . createHTML ( ` 0 товаров • ${ 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' ) ;
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 ( ) } ) ;
}
} ) ;
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 ) => {
const avatar = '🥦' ;
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 ] ;
let isCategoriesExpanded = false ;
const maxVisibleCategories = 9 ;
const sendUpdatedCategories = debounce ( async ( ) => {
return setOption ( 'categories' , selectedCategories ) ;
} , 1500 ) ;
const categoriesGrid = document . getElementById ( 'categories-grid' ) ;
const toggleBtn = document . getElementById ( 'expand-categories' ) ;
function renderCategories ( ) {
2026-08-21 15:23:20 +00:00
categoriesGrid . innerHTML = cartPolicy . createHTML ( '' ) ;
2026-08-21 14:52:45 +00:00
const dataToShow = isCategoriesExpanded ? categories : categories . slice ( 0 , maxVisibleCategories ) ;
dataToShow . forEach ( cat => {
const btn = document . createElement ( 'button' ) ;
btn . className = ` category-btn ${ selectedCategories . includes ( cat ) ? 'active' : 'inactive' } ` ;
btn . textContent = cat ;
btn . dataset . category = cat ;
btn . addEventListener ( 'click' , function ( ) {
const catName = this . dataset . category ;
if ( selectedCategories . includes ( catName ) ) {
selectedCategories = selectedCategories . filter ( c => c !== catName ) ;
} else {
selectedCategories . push ( catName ) ;
}
renderCategories ( ) ;
sendUpdatedCategories ( ) ;
} ) ;
categoriesGrid . appendChild ( btn ) ;
} ) ;
if ( isCategoriesExpanded ) {
toggleBtn . classList . remove ( 'collapsed' ) ;
} else {
toggleBtn . classList . add ( 'collapsed' ) ;
}
}
toggleBtn . addEventListener ( 'click' , function ( ) {
isCategoriesExpanded = ! isCategoriesExpanded ;
renderCategories ( ) ;
} ) ;
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 ) ;
} ) ;
} ) ;