/
mikhalitsyn
/
menu
Обзор
Документация
Войти
/
mikhalitsyn
/
menu
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
menu_data_week.html
1 237 строк
43 KB
mikhalitsyn
Create: menu_data.json, menu_data_week.html
31 июл 2026, 12:42
Верифицирован
31 июл 2026, 12:42
bd0ff86
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>Меню на неделю — Reels</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Arial, sans-serif; background: #000; min-height: 100vh; overflow: hidden; user-select: none; -webkit-user-select: none; } .container { max-width: 100%; margin: 0; padding: 0; border-radius: 0; box-shadow: none; background: #000; min-height: 100vh; display: flex; flex-direction: column; position: relative; width: 100%; height: 100vh; overflow: hidden; } .header { position: absolute; top: 0; left: 0; right: 0; z-index: 20; padding: 20px 20px 12px; border-bottom: none; margin-bottom: 0; background: linear-gradient(180deg, rgba(0,0,0,0.8) 0%, transparent 100%); pointer-events: none; display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; } .header .day-name { color: #fff; font-size: 28px; font-weight: 800; text-shadow: 0 2px 8px rgba(0,0,0,0.7); letter-spacing: 0.5px; } .header .day-name .today-badge { background: #fff; color: #1f4a1e; font-size: 12px; padding: 2px 12px; border-radius: 30px; display: inline-block; margin-left: 6px; vertical-align: middle; } .header .day-total { color: #ffd700; font-size: 16px; text-shadow: 0 2px 8px rgba(0,0,0,0.7); } .header .header-time { color: rgba(255,255,255,0.95); font-size: 32px; font-weight: 800; text-shadow: 0 2px 12px rgba(0,0,0,0.8); letter-spacing: 1px; line-height: 1; text-align: right; margin-top: 4px; } .header-left { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; flex: 1; } .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; } .header-products { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.06); width: 100%; } .header-products .product-tag { color: rgba(255,255,255,0.4); font-size: 18px; font-weight: 400; padding: 2px 0; line-height: 1.4; background: none; border: none; text-shadow: 0 2px 8px rgba(0,0,0,0.7); } .day-slider-container { flex: 1; position: relative; overflow: hidden; touch-action: none; width: 100%; height: 100vh; cursor: grab; } .day-slider-container:active { cursor: grabbing; } .day-slider-track { display: flex; height: 100%; transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); will-change: transform; } .day-slide { min-width: 100%; height: 100%; position: relative; overflow: hidden; } .dish-slider-container { width: 100%; height: 100%; position: relative; overflow: hidden; touch-action: none; cursor: grab; } .dish-slider-container:active { cursor: grabbing; } .dish-slider-track { height: 100%; transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); will-change: transform; display: flex; flex-direction: column; } .dish-slide { width: 100%; height: 100%; flex-shrink: 0; position: relative; display: flex; align-items: center; justify-content: center; } .meal-card { width: 100%; height: 100%; position: relative; background: #000; display: flex; align-items: center; justify-content: center; overflow: hidden; } .meal-card .meal-image { width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; display: block; } .meal-card .meal-image.empty { display: flex; align-items: center; justify-content: center; font-size: 100px; color: #555; background: #1a1a1a; position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .meal-card .meal-body { position: absolute; bottom: 0; left: 0; right: 0; padding: 30px 24px 140px; background: linear-gradient(0deg, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.4) 50%, transparent 100%); color: #fff; z-index: 10; pointer-events: none; } .meal-card .meal-header { margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; flex-direction: row; } .meal-card .meal-label { color: #fff; font-size: 30px; font-weight: 800; text-shadow: 0 4px 16px rgba(0,0,0,0.9); letter-spacing: 0.5px; } .meal-card .meal-label.snack { color: #ffd700; } .meal-card .meal-text { color: #fff; font-size: 24px; font-weight: 500; text-shadow: 0 4px 16px rgba(0,0,0,0.9); line-height: 1.4; margin-top: 2px; text-align: center; } .meal-card .meal-bottom { margin-top: 10px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; width: 100%; } .meal-card .meal-kcal { color: #ffd700; font-size: 18px; font-weight: 700; text-shadow: 0 4px 16px rgba(0,0,0,0.9); flex-shrink: 0; } .meal-card .meal-bread { color: #ffd700; font-size: 16px; font-weight: 500; text-shadow: 0 4px 16px rgba(0,0,0,0.9); flex-shrink: 0; } .dish-dots { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 14px; z-index: 14; pointer-events: auto; } .dish-dot { width: 12px; height: 12px; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.05); border-radius: 50%; display: block; transition: 0.3s; cursor: pointer; } .dish-dot.active { background: #fff; width: 18px; height: 18px; border-color: #fff; box-shadow: 0 0 20px rgba(255,255,255,0.15); } .day-dots { position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; z-index: 14; pointer-events: auto; } .day-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.2); border: none; transition: 0.3s; cursor: pointer; padding: 0; } .day-dot.active { background: #ffd700; width: 24px; border-radius: 10px; } .day-nav { position: absolute; bottom: 24px; left: 0; right: 0; z-index: 15; padding: 0 20px; display: flex; justify-content: space-between; align-items: center; pointer-events: none; } .day-nav-btn { color: #fff; font-size: 36px; text-shadow: 0 2px 12px rgba(0,0,0,0.8); padding: 10px 18px; background: rgba(0,0,0,0.3); border-radius: 50%; width: 54px; height: 54px; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.08); transition: 0.3s; pointer-events: auto; cursor: pointer; } .day-nav-btn:hover { background: rgba(255,255,255,0.12); } .day-nav .day-label { color: #fff; font-size: 20px; font-weight: 700; text-shadow: 0 2px 12px rgba(0,0,0,0.8); pointer-events: none; } .toast { position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: #fff; padding: 10px 24px; border-radius: 30px; font-size: 16px; font-weight: 600; opacity: 0; transition: opacity 0.4s ease; pointer-events: none; z-index: 999; box-shadow: 0 4px 16px rgba(0,0,0,0.4); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.05); } .toast.show { opacity: 1; } .toast.error { background: rgba(180, 30, 30, 0.9); } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; color: #888; font-size: 16px; gap: 16px; } .loading .spinner { width: 40px; height: 40px; border: 3px solid #333; border-top: 3px solid #ffd700; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; color: #ff6b6b; font-size: 16px; gap: 12px; text-align: center; padding: 20px; } .error .icon { font-size: 48px; } .error button { padding: 10px 30px; border: none; border-radius: 30px; background: #ffd700; color: #000; font-weight: 600; font-size: 16px; cursor: pointer; margin-top: 8px; } @media (max-width: 480px) { .header .day-name { font-size: 22px; } .header .header-time { font-size: 26px; } .meal-card .meal-label { font-size: 26px; } .meal-card .meal-text { font-size: 20px; } .header-products .product-tag { font-size: 16px; } .day-nav-btn { width: 44px; height: 44px; font-size: 28px; padding: 6px 12px; } .dish-dot { width: 10px; height: 10px; } .dish-dot.active { width: 14px; height: 14px; } } </style> </head> <body> <div class="container" id="app"> <div id="content"> <div class="loading"> <div class="spinner"></div> <span>Загрузка меню...</span> </div> </div> </div> <div class="toast" id="toast"></div> <script> // ===== CONFIG ===== const MEAL_ORDER = ['Завтрак', 'Перекус', 'Обед', 'Полдник', 'Ужин']; const MEAL_TIMES = { 'Завтрак': '08:00', 'Перекус': '10:00', 'Обед': '14:00', 'Полдник': '16:00', 'Ужин': '19:00' }; const DAY_NAMES = { mon: 'Понедельник', tue: 'Вторник', wed: 'Среда', thu: 'Четверг', fri: 'Пятница', sat: 'Суббота', sun: 'Воскресенье' }; const DAY_ORDER = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; const DAY_LABELS_SHORT = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'ВС']; let menuData = null; let currentDayIndex = 0; let currentDishIndex = 0; let dayDishes = []; const content = document.getElementById('content'); const toastEl = document.getElementById('toast'); // ===== HELPERS ===== function getTodayIndex() { const today = new Date().getDay(); return today === 0 ? 6 : today - 1; } function getDayKey(index) { return DAY_ORDER[index]; } function parseKcal(kcalStr) { if (!kcalStr) return 0; const cleaned = String(kcalStr).replace(/[^0-9]/g, ''); return parseInt(cleaned) || 0; } function getDayTotal(dayData) { if (!dayData || !dayData.meals) return 0; return dayData.meals.reduce((sum, m) => sum + parseKcal(m.kcal), 0); } function getMealTime(label) { return MEAL_TIMES[label] || ''; } function getFilteredMeals(day) { if (!day || !day.meals) return []; return day.meals .filter(m => MEAL_ORDER.includes(m.label)) .sort((a, b) => MEAL_ORDER.indexOf(a.label) - MEAL_ORDER.indexOf(b.label)); } function getProductsForMeal(meal) { if (!meal || !meal.products || !Array.isArray(meal.products)) return []; return meal.products.map(p => p.trim()).filter(p => p); } function showToast(message, isError = false) { toastEl.textContent = message; toastEl.className = 'toast' + (isError ? ' error' : ''); toastEl.classList.add('show'); clearTimeout(toastEl._timeout); toastEl._timeout = setTimeout(() => { toastEl.classList.remove('show'); }, 2000); } // ===== PREPARE DATA ===== function prepareDayDishes(menuData) { const result = []; for (let i = 0; i < 7; i++) { const dayKey = getDayKey(i); const day = menuData[dayKey]; if (day) { const meals = getFilteredMeals(day); result.push(meals); } else { result.push([]); } } return result; } // ===== RENDER ===== function render() { if (!menuData) return; dayDishes = prepareDayDishes(menuData); if (currentDayIndex >= dayDishes.length) currentDayIndex = 0; const dishes = dayDishes[currentDayIndex] || []; if (currentDishIndex >= dishes.length) currentDishIndex = 0; let html = ''; const dayKey = getDayKey(currentDayIndex); const day = menuData[dayKey]; const total = getDayTotal(day); const isToday = currentDayIndex === getTodayIndex(); const currentMeal = dishes[currentDishIndex] || dishes[0] || null; const currentTime = currentMeal ? getMealTime(currentMeal.label) : ''; const currentProducts = currentMeal ? getProductsForMeal(currentMeal) : []; html += ` <div class="header"> <div class="header-left"> <div class="day-name"> ${DAY_NAMES[dayKey]} ${isToday ? '<span class="today-badge">⭐ Сегодня</span>' : ''} </div> <div class="day-total">🔥 ${total} ккал</div> ${currentProducts.length > 0 ? ` <div class="header-products"> ${currentProducts.map(p => `<span class="product-tag">${p}</span>`).join('')} </div> ` : ''} </div> <div class="header-right"> <div class="header-time">${currentTime}</div> </div> </div> `; html += ` <div class="day-slider-container" id="daySliderContainer"> <div class="day-slider-track" id="daySliderTrack" style="transform: translateX(-${currentDayIndex * 100}%);"> `; for (let d = 0; d < dayDishes.length; d++) { const dishesForDay = dayDishes[d] || []; html += ` <div class="day-slide" data-day="${d}"> <div class="dish-slider-container" id="dishSliderContainer_${d}"> <div class="dish-slider-track" id="dishSliderTrack_${d}" style="transform: translateY(-${(d === currentDayIndex ? currentDishIndex : 0) * 100}%);"> `; if (dishesForDay.length === 0) { html += ` <div class="dish-slide"> <div class="meal-card"> <div class="meal-image empty">🍽️</div> <div class="meal-body"> <div style="text-align:center;color:rgba(255,255,255,0.4);font-size:20px;">Нет блюд</div> </div> </div> </div> `; } else { dishesForDay.forEach((meal, idx) => { const img = meal.image || ''; const lbl = meal.label || ''; const isSn = lbl === 'Перекус' || lbl === 'Полдник'; const kc = parseKcal(meal.kcal); const br = meal.bread || ''; const text = meal.text || ''; html += ` <div class="dish-slide" data-day="${d}" data-dish="${idx}"> <div class="meal-card"> ${img ? `<img src="${img}" class="meal-image" alt="${lbl}" onerror="this.className='meal-image empty';this.alt='🖼️'">` : `<div class="meal-image empty">🖼️</div>`} <div class="meal-body"> <div class="meal-header"> <span class="meal-label${isSn ? ' snack' : ''}">${lbl}</span> </div> <div class="meal-text">${text}</div> <div class="meal-bottom"> <span class="meal-kcal">🔥 ${kc} ккал</span> ${br ? `<span class="meal-bread">${br}</span>` : ''} </div> </div> </div> </div> `; }); } html += ` </div> </div> </div> `; } html += ` </div> </div> `; const currentDishes = dayDishes[currentDayIndex] || []; if (currentDishes.length > 1) { html += ` <div class="dish-dots" id="dishDots"> `; currentDishes.forEach((_, idx) => { html += ` <span class="dish-dot${idx === currentDishIndex ? ' active' : ''}" data-dish="${idx}"></span> `; }); html += ` </div> `; } html += ` <div class="day-dots" id="dayDots"> `; for (let d = 0; d < dayDishes.length; d++) { html += ` <span class="day-dot${d === currentDayIndex ? ' active' : ''}" data-day="${d}"></span> `; } html += ` </div> `; html += ` <div class="day-nav"> <button class="day-nav-btn" id="prevDayBtn">‹</button> <span class="day-label">${DAY_LABELS_SHORT[currentDayIndex]}</span> <button class="day-nav-btn" id="nextDayBtn">›</button> </div> `; content.innerHTML = html; initDaySlider(); initDishSliders(); initDishDots(); initDayDots(); initDayNavButtons(); initKeyboardEvents(); initSwipeEvents(); } // ===== ИНИЦИАЛИЗАЦИЯ ===== function initDaySlider() { const track = document.getElementById('daySliderTrack'); if (!track) return; track.style.transform = `translateX(-${currentDayIndex * 100}%)`; } function initDishSliders() { for (let d = 0; d < dayDishes.length; d++) { const track = document.getElementById(`dishSliderTrack_${d}`); if (!track) continue; const translateY = -(d === currentDayIndex ? currentDishIndex : 0) * 100; track.style.transform = `translateY(${translateY}%)`; } } function initDishDots() { document.querySelectorAll('.dish-dot').forEach(dot => { dot.addEventListener('click', function() { const idx = parseInt(this.dataset.dish); goToDish(idx); }); }); } function initDayDots() { document.querySelectorAll('.day-dot').forEach(dot => { dot.addEventListener('click', function() { const idx = parseInt(this.dataset.day); goToDay(idx); }); }); } function initDayNavButtons() { const prevBtn = document.getElementById('prevDayBtn'); const nextBtn = document.getElementById('nextDayBtn'); if (prevBtn) prevBtn.addEventListener('click', () => goToDay(currentDayIndex - 1)); if (nextBtn) nextBtn.addEventListener('click', () => goToDay(currentDayIndex + 1)); } // ===== НАВИГАЦИЯ ===== function goToDay(index) { if (index < 0 || index >= dayDishes.length) { showToast(index < 0 ? '📅 Это первый день' : '📅 Это последний день'); return; } if (index === currentDayIndex) return; currentDayIndex = index; currentDishIndex = 0; const track = document.getElementById('daySliderTrack'); if (track) { track.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; track.style.transform = `translateX(-${currentDayIndex * 100}%)`; } for (let d = 0; d < dayDishes.length; d++) { const dishTrack = document.getElementById(`dishSliderTrack_${d}`); if (!dishTrack) continue; const translateY = -(d === currentDayIndex ? 0 : 0) * 100; dishTrack.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; dishTrack.style.transform = `translateY(${translateY}%)`; } setTimeout(() => { updateUI(); updateDishDots(); updateDayDots(); }, 50); } function goToDish(index) { const dishes = dayDishes[currentDayIndex] || []; if (index < 0 || index >= dishes.length) return; if (index === currentDishIndex) return; currentDishIndex = index; const track = document.getElementById(`dishSliderTrack_${currentDayIndex}`); if (track) { track.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; track.style.transform = `translateY(-${currentDishIndex * 100}%)`; } setTimeout(() => { updateUI(); updateDishDots(); updateProductsInHeader(); }, 50); } function nextDish() { const dishes = dayDishes[currentDayIndex] || []; if (dishes.length === 0) return; if (currentDishIndex < dishes.length - 1) { goToDish(currentDishIndex + 1); } else { showToast('🍽️ Это последнее блюдо в дне'); } } function prevDish() { const dishes = dayDishes[currentDayIndex] || []; if (dishes.length === 0) return; if (currentDishIndex > 0) { goToDish(currentDishIndex - 1); } else { showToast('🍽️ Это первое блюдо в дне'); } } // ===== ОБНОВЛЕНИЕ UI ===== function updateUI() { const dayKey = getDayKey(currentDayIndex); const day = menuData[dayKey]; const total = getDayTotal(day); const isToday = currentDayIndex === getTodayIndex(); const dishes = dayDishes[currentDayIndex] || []; const currentMeal = dishes[currentDishIndex] || dishes[0] || null; const currentTime = currentMeal ? getMealTime(currentMeal.label) : ''; const currentProducts = currentMeal ? getProductsForMeal(currentMeal) : []; const dayNameEl = document.querySelector('.header .day-name'); if (dayNameEl) { dayNameEl.innerHTML = ` ${DAY_NAMES[dayKey]} ${isToday ? '<span class="today-badge">⭐ Сегодня</span>' : ''} `; } const totalEl = document.querySelector('.header .day-total'); if (totalEl) totalEl.textContent = `🔥 ${total} ккал`; const timeEl = document.querySelector('.header .header-time'); if (timeEl) timeEl.textContent = currentTime; const productsContainer = document.querySelector('.header-products'); if (productsContainer) { if (currentProducts.length > 0) { productsContainer.innerHTML = currentProducts.map(p => `<span class="product-tag">${p}</span>`).join(''); } else { productsContainer.innerHTML = ''; } } const dayLabel = document.querySelector('.day-nav .day-label'); if (dayLabel) dayLabel.textContent = DAY_LABELS_SHORT[currentDayIndex]; updateDayDots(); } function updateDishDots() { document.querySelectorAll('.dish-dot').forEach((dot, idx) => { dot.classList.toggle('active', idx === currentDishIndex); }); } function updateDayDots() { document.querySelectorAll('.day-dot').forEach((dot, idx) => { dot.classList.toggle('active', idx === currentDayIndex); }); } function updateProductsInHeader() { const dishes = dayDishes[currentDayIndex] || []; const currentMeal = dishes[currentDishIndex] || dishes[0] || null; const currentProducts = currentMeal ? getProductsForMeal(currentMeal) : []; const productsContainer = document.querySelector('.header-products'); if (productsContainer) { if (currentProducts.length > 0) { productsContainer.innerHTML = currentProducts.map(p => `<span class="product-tag">${p}</span>`).join(''); } else { productsContainer.innerHTML = ''; } } } // ===== KEYBOARD EVENTS ===== function initKeyboardEvents() { document.addEventListener('keydown', function(e) { if (e.key === 'ArrowLeft') { e.preventDefault(); goToDay(currentDayIndex - 1); } else if (e.key === 'ArrowRight') { e.preventDefault(); goToDay(currentDayIndex + 1); } else if (e.key === 'ArrowDown') { e.preventDefault(); nextDish(); } else if (e.key === 'ArrowUp') { e.preventDefault(); prevDish(); } }); } // ===== SWIPE EVENTS (полностью переписаны с нуля) ===== function initSwipeEvents() { const container = document.getElementById('daySliderContainer'); if (!container) return; let startX = 0; let startY = 0; let isSwiping = false; let currentX = 0; let currentY = 0; let isDaySwipe = false; let isDishSwipe = false; // --- Обработчик начала касания/нажатия --- function onStart(clientX, clientY, target) { if (target.closest('.dish-dot') || target.closest('.day-dot') || target.closest('.day-nav-btn')) { return; } startX = clientX; startY = clientY; isSwiping = true; currentX = clientX; currentY = clientY; isDaySwipe = false; isDishSwipe = false; } // --- Обработчик движения --- function onMove(clientX, clientY) { if (!isSwiping) return; const deltaX = clientX - startX; const deltaY = clientY - startY; if (!isDaySwipe && !isDishSwipe) { if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { if (Math.abs(deltaX) > Math.abs(deltaY)) { isDaySwipe = true; } else { isDishSwipe = true; } } return; } if (isDaySwipe) { const track = document.getElementById('daySliderTrack'); if (track) { const containerWidth = container.offsetWidth || window.innerWidth; const percent = (deltaX / containerWidth) * 100; const currentTranslate = -currentDayIndex * 100; const newTranslate = currentTranslate + percent; const maxTranslate = 0; const minTranslate = -(dayDishes.length - 1) * 100; const clamped = Math.max(minTranslate, Math.min(maxTranslate, newTranslate)); track.style.transition = 'none'; track.style.transform = `translateX(${clamped}%)`; } } if (isDishSwipe) { const dishes = dayDishes[currentDayIndex] || []; if (dishes.length <= 1) return; const track = document.getElementById(`dishSliderTrack_${currentDayIndex}`); if (track) { const containerHeight = container.offsetHeight || window.innerHeight; const percent = (deltaY / containerHeight) * 100; const currentTranslate = -currentDishIndex * 100; const newTranslate = currentTranslate + percent; const maxTranslate = 0; const minTranslate = -(dishes.length - 1) * 100; const clamped = Math.max(minTranslate, Math.min(maxTranslate, newTranslate)); track.style.transition = 'none'; track.style.transform = `translateY(${clamped}%)`; } } currentX = clientX; currentY = clientY; } // --- Обработчик завершения --- function onEnd() { if (!isSwiping) return; isSwiping = false; if (isDaySwipe) { const track = document.getElementById('daySliderTrack'); if (track) { const style = window.getComputedStyle(track); const matrix = style.transform; let currentTranslate = 0; if (matrix !== 'none') { const values = matrix.match(/matrix.*\((.+)\)/); if (values) { const nums = values[1].split(', '); currentTranslate = parseFloat(nums[4]) || 0; } } const slideWidth = 100; const ratio = Math.round(-currentTranslate / slideWidth); const targetIndex = Math.max(0, Math.min(dayDishes.length - 1, ratio)); if (targetIndex !== currentDayIndex) { goToDay(targetIndex); } else { const track = document.getElementById('daySliderTrack'); if (track) { track.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; track.style.transform = `translateX(-${currentDayIndex * 100}%)`; } } } } if (isDishSwipe) { const dishes = dayDishes[currentDayIndex] || []; if (dishes.length <= 1) return; const track = document.getElementById(`dishSliderTrack_${currentDayIndex}`); if (track) { const style = window.getComputedStyle(track); const matrix = style.transform; let currentTranslate = 0; if (matrix !== 'none') { const values = matrix.match(/matrix.*\((.+)\)/); if (values) { const nums = values[1].split(', '); currentTranslate = parseFloat(nums[5]) || 0; } } const slideHeight = 100; const ratio = Math.round(-currentTranslate / slideHeight); const targetIndex = Math.max(0, Math.min(dishes.length - 1, ratio)); if (targetIndex !== currentDishIndex) { goToDish(targetIndex); } else { const track = document.getElementById(`dishSliderTrack_${currentDayIndex}`); if (track) { track.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; track.style.transform = `translateY(-${currentDishIndex * 100}%)`; } } } } isDaySwipe = false; isDishSwipe = false; } // --- Mouse events --- container.addEventListener('mousedown', function(e) { onStart(e.clientX, e.clientY, e.target); }); document.addEventListener('mousemove', function(e) { onMove(e.clientX, e.clientY); }); document.addEventListener('mouseup', function() { onEnd(); }); // --- Touch events --- container.addEventListener('touchstart', function(e) { const touch = e.touches[0]; onStart(touch.clientX, touch.clientY, e.target); }, { passive: true }); container.addEventListener('touchmove', function(e) { const touch = e.touches[0]; onMove(touch.clientX, touch.clientY); }, { passive: true }); container.addEventListener('touchend', function() { onEnd(); }, { passive: true }); // --- Wheel --- container.addEventListener('wheel', function(e) { e.preventDefault(); if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 15) { if (e.deltaX > 0) goToDay(currentDayIndex + 1); else goToDay(currentDayIndex - 1); } else if (Math.abs(e.deltaY) > Math.abs(e.deltaX) && Math.abs(e.deltaY) > 15) { if (e.deltaY > 0) nextDish(); else prevDish(); } }, { passive: false }); } // ===== LOAD MENU ===== function loadMenu() { content.innerHTML = ` <div class="loading"> <div class="spinner"></div> <span>Загрузка меню...</span> </div> `; fetch('menu_data.json') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(data => { let converted; if (data.menu && data.menu.rows) { const rows = data.menu.rows; converted = {}; rows.forEach(row => { const label = row.label; row.cells.forEach((cell, idx) => { const dayKey = DAY_ORDER[idx]; if (!converted[dayKey]) { converted[dayKey] = { meals: [] }; } converted[dayKey].meals.push({ label: label, text: cell.dish || '', image: cell.photo || '', kcal: cell.products.reduce((sum, p) => sum + (p.cal || 0), 0), products: cell.products.map(p => p.name || '') }); }); }); } else if (data.rows) { const rows = data.rows; converted = {}; rows.forEach(row => { const label = row.label; row.cells.forEach((cell, idx) => { const dayKey = DAY_ORDER[idx]; if (!converted[dayKey]) { converted[dayKey] = { meals: [] }; } converted[dayKey].meals.push({ label: label, text: cell.dish || '', image: cell.photo || '', kcal: cell.products.reduce((sum, p) => sum + (p.cal || 0), 0), products: cell.products.map(p => p.name || '') }); }); }); } else { converted = data; } menuData = converted; currentDayIndex = getTodayIndex(); currentDishIndex = 0; render(); showToast('✅ Меню загружено'); }) .catch(err => { console.error('Error:', err); content.innerHTML = ` <div class="error"> <span class="icon">📂</span> Не удалось загрузить <strong>menu_data.json</strong> <span style="color:#888;font-size:14px;">${err.message}</span> <button onclick="loadMenu()">🔄 Попробовать снова</button> </div> `; }); } // ===== INIT ===== loadMenu(); </script> </body> </html>