Загрузка данных
/* ===========================================================================
MPULSE — script.js
0. Форма: валидация + отправка
1. Лайтбокс сертификатов
2. Прелоадер / тема / шапка
2.5 HERO: загрузка фона images/hero.jpg + отладка
3. Поп-апы
4. Графики кейсов
5. Reveal, счётчики
(блок FAQ удалён вместе с секцией на странице)
=========================================================================== */
/* ===========================================================================
0. ФОРМА: валидация + отправка (FormSubmit)
=========================================================================== */
(function(){
const EMAIL = 'mpulse@gmail.com';
const form = document.getElementById('form');
if(!form) return;
const okBox = document.getElementById('ok'),
errBox= document.getElementById('err'),
btn = document.getElementById('submitBtn');
const fld = n => form.querySelector('[name="'+n+'"]');
const fieldBox = n => form.querySelector('[data-field="'+n+'"]');
const clearErr = () => {
form.querySelectorAll('.field.bad').forEach(f=>f.classList.remove('bad'));
form.querySelectorAll('.field-err.show').forEach(s=>s.classList.remove('show'));
};
const mark = (n, errKey) => {
const f = fieldBox(n);
if(!f) return;
f.classList.add('bad');
f.querySelectorAll('.field-err').forEach(s=>{
if(!s.dataset.err || s.dataset.err === errKey) s.classList.add('show');
});
const inp = f.querySelector('input');
if(inp) inp.setAttribute('aria-invalid','true');
};
const focusFirstBad = () => {
const first = form.querySelector('.field.bad input');
if(first){
first.focus({preventScroll:true});
first.scrollIntoView({block:'center',behavior:'smooth'});
}
};
form.querySelectorAll('input,textarea,select').forEach(el=>{
el.addEventListener('input', ()=>{
const f = el.closest('.field');
if(f){
f.classList.remove('bad');
f.querySelectorAll('.field-err.show').forEach(s=>s.classList.remove('show'));
}
el.removeAttribute('aria-invalid');
});
});
const isMail = v => /^[^\s@]+@[^\s@]+\.[a-z-]{2,}$/i.test(v.trim());
const isContact = v => {
v = v.trim();
if(v.replace(/\D/g,'').length >= 7) return true;
if(/t\.me\//i.test(v)) return true;
if(/^@?[a-z0-9_.]{4,}$/i.test(v)) return true;
return false;
};
form.addEventListener('submit', async e => {
e.preventDefault();
okBox.style.display = errBox.style.display = 'none';
clearErr();
const name = (fld('name').value || '').trim(),
phone = (fld('phone').value || '').trim(),
mail = (fld('email').value || '').trim();
if(name.length < 2) mark('name');
if(!phone && !mail){ mark('phone'); mark('email','contact'); }
if(phone && !isContact(phone) && !(mail && isMail(mail))){ mark('phone'); mark('email','contact'); }
if(mail && !isMail(mail) && !(phone && isContact(phone))){ mark('email','mail'); }
if(form.querySelector('.field.bad')){ focusFirstBad(); return; }
const fd = new FormData(form);
fd.append('_subject','MPULSE — новая заявка с сайта');
fd.append('_template','table');
fd.append('_captcha','false');
/* фиксируем факт согласия на обработку ПД (для юр. подстраховки) */
fd.append('Согласие на обработку ПД','Дано нажатием кнопки отправки, ' + new Date().toLocaleString('ru-RU'));
const label = btn.textContent;
btn.disabled = true; btn.textContent = 'Отправляем…';
try{
const r = await fetch('https://formsubmit.co/ajax/'+EMAIL, {
method:'POST', headers:{'Accept':'application/json'}, body:fd
});
if(!r.ok) throw new Error('bad status');
okBox.style.display = 'block';
form.reset();
}catch(err){
errBox.style.display = 'block';
}finally{
btn.disabled = false; btn.textContent = label;
setTimeout(()=>{ okBox.style.display = errBox.style.display = 'none'; }, 8000);
}
});
document.querySelectorAll('[data-goform]').forEach(el=>{
el.addEventListener('click', ev=>{
ev.preventDefault();
const c = document.getElementById('contacts');
if(c) c.scrollIntoView({behavior:'smooth',block:'start'});
setTimeout(()=>{ const n=fld('name'); if(n) n.focus({preventScroll:true}); }, 700);
});
});
})();
/* ===========================================================================
1. ЛАЙТБОКС СЕРТИФИКАТОВ
=========================================================================== */
(function(){
const lb = document.getElementById('lb'), lbImg = document.getElementById('lbImg');
if(!lb || !lbImg) return;
const open = src => { lbImg.src = src; lb.classList.add('open'); document.documentElement.style.overflow='hidden'; };
const close = () => { lb.classList.remove('open'); document.documentElement.style.overflow=''; setTimeout(()=>{lbImg.src='';},300); };
document.querySelectorAll('[data-zoom]').forEach(el=>{
el.addEventListener('click', ()=>{
if(el.querySelector('img')) open(el.dataset.zoom);
});
});
lb.addEventListener('click', e=>{ if(e.target === lb || e.target.closest('[data-lb-close]')) close(); });
document.addEventListener('keydown', e=>{ if(e.key === 'Escape') close(); });
})();
/* ---------- 2. Прелоадер ---------- */
(function(){
const num = document.getElementById('ldNum'), ld = document.getElementById('loader');
if(!num || !ld) return;
let n = 0;
const t = setInterval(()=>{
n = Math.min(100, n + Math.ceil(Math.random()*18));
num.textContent = n;
if(n === 100){ clearInterval(t); setTimeout(()=>{ ld.classList.add('done'); document.body.classList.add('loaded'); },180); }
},24);
})();
/* ===========================================================================
2.5 HERO-ФОН: images/hero.jpg
Причина прежней «неработающей» загрузки: тег <img> в разметке был
закомментирован (<!-- ... -->), поэтому браузер его не запрашивал.
Ниже — контроль загрузки + понятная диагностика в консоли.
=========================================================================== */
(function(){
const box = document.querySelector('.hero-bg');
if(!box) return;
const img = box.querySelector('img');
if(!img){
box.classList.add('no-img');
console.warn('[MPULSE hero] тег <img> в .hero-bg отсутствует — используется градиентный фон.');
return;
}
const ok = () => {
box.classList.add('has-img');
console.info('[MPULSE hero] фон загружен:', img.currentSrc || img.src,
img.naturalWidth + '×' + img.naturalHeight);
};
const fail = () => {
box.classList.add('no-img');
console.warn('[MPULSE hero] не удалось загрузить «' + img.getAttribute('src') + '».\n' +
'Проверьте: 1) файл лежит в папке images рядом с index.html; ' +
'2) имя и расширение точно совпадают, включая регистр (hero.jpg ≠ Hero.JPG ≠ hero.jpeg); ' +
'3) файл не заблокирован сервером/CDN (откройте адрес images/hero.jpg напрямую в браузере). ' +
'Пока файла нет — показывается градиентный фон.');
img.remove();
};
if(img.complete){ img.naturalWidth > 0 ? ok() : fail(); }
else { img.addEventListener('load', ok, {once:true}); img.addEventListener('error', fail, {once:true}); }
})();
/* ---------- Тема ---------- */
const root = document.documentElement, themeBtn = document.getElementById('themeBtn');
function setTheme(t){
root.setAttribute('data-theme', t);
try{ localStorage.setItem('mpulse-theme', t); }catch(e){}
if(themeBtn) themeBtn.textContent = t === 'dark' ? '☀' : '☾';
}
setTheme((()=>{ try{ return localStorage.getItem('mpulse-theme'); }catch(e){ return null; } })()
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
if(themeBtn) themeBtn.onclick = ()=> setTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
/* ---------- Шапка / прогресс / бургер ---------- */
const hdr = document.getElementById('hdr'), prog = document.getElementById('prog');
let lastY = 0;
addEventListener('scroll', ()=>{
const y = scrollY;
if(hdr){
hdr.classList.toggle('scrolled', y > 10);
hdr.classList.toggle('hide', y > 300 && y > lastY);
}
if(prog){
const h = document.documentElement.scrollHeight - innerHeight;
prog.style.width = (h > 0 ? y/h*100 : 0) + '%';
}
lastY = y;
});
/* null-guard'ы: элементы могут отсутствовать на юр. страницах */
(function(){
const burger = document.getElementById('burger'), menu = document.getElementById('menu');
if(burger && menu) burger.onclick = ()=> menu.classList.toggle('show');
if(menu) menu.querySelectorAll('a').forEach(a=> a.onclick = ()=> menu.classList.remove('show'));
const yr = document.getElementById('year');
if(yr) yr.textContent = new Date().getFullYear();
})();
/* ===========================================================================
3. ПОП-АПЫ
=========================================================================== */
const MODALS = {
launch:{title:'Запуск бренда с нуля',
text:'Берём проект на старте: регистрация, юнит-экономика, поставки и первые карточки. Один из наших брендов вырос с 0 до 2,3 млн рублей заказов в месяц за 6 месяцев.'},
scale:{title:'Масштабирование продаж',
text:'Увеличиваем продажи действующих селлеров. Все начинается с аудита кабинета - анализируем карточки, рекламу, экономические планы, после чего мы вместе составляем план роста. Для корректировки плана мы ориентруемся на ежемесячные показатели.'},
ads:{title:'Реклама на маркетплейсах',
text:'Настраиваем и ведем внутреннюю рекламу на площадках, снижаем ДРР и повышаем продажи. В наши услуги также включены:',
list:['разработка стратегии и структуры рекламы','Работа с поисковыми запросами','A/B-тесты креативов и карточек.']},
analytics:{title:'Аналитика и юнит-экономика',
text:'Анализируем статистику, чтобы каждое решение было взвешенным.',
list:['Работаем как в формате дашбордов, так и в традиционных отчётах и планах.','Планируем юнит-экономику по каждому артикулу.','Анализируем ценообразование, проводим контроль маржинальности.']},
content:{title:'Контент и SEO',
text:'Поможем сделать карточки, которые выведут ваши товары в топ поисковой выдачи: тексты, ключевые запросы, методические рекоммендации по фото и инфографике (A/B-тесты обложек, Rich-контент, создание видео и пр.).'},
supply:{title:'Управление поставками',
text:'Спланируем поставки так, чтобы не допустить обнуления и заморозки остатков. Для этого мы составляем прогноз спроса и сезонности, создаем план отгрузок, контролируем остатки по складам и пр.'},
reviews:{title:'Работа с отзывами',
text:'Управляем рейтингом товаров, повышаем конверсию, работаем с обратной связью: отвечаем на отзывы и вопросы, работаем с негативными отзывами, повышаем рейтинг карточек.' },
crisis:{title:'Кризисный отдел',
text:'Быстро реагируем на блокировки, штрафы и падения продаж. Помогаем вернуть прежние показатели; при необходимости - оспариваем штрафы, работаем с поддержкой площадок.'},
con1:{title:'Долгий поиск сотрудников',
text:'Поиск сильного менеджера маркетплейсов занимает несколько месяцев; долгий отбор и собеседования, затраты на рекрутинг, риск ошибиться с наймом - всё это время бюджет теряет деньги.'},
con2:{title:'Текучка кадров',
text:'В кризисный период текучка кадров может существенно ударить по всему плану развития бизнеса. А уход ключевого специалиста в сфере работы с маркетплейсами - это существенная потеря темпов роста и наработанного опыта. '},
con3:{title:'Обучение и адаптация',
text:'Новым сотрудникам требуется обучение, на которое уходит ваше время и деньги. От найма до полного включения в работу в среднем уходит до полугода. Поиск отдела на аутсорсе исключает затраты на курсы и наставников.'},
con4:{title:'Недостаток мотивации',
text:'Штатный сотрудник получает оклад независимо от результата рост кабинета — у него нет личной заинтересованности в развитии вашей компании.' },
pro1:{title:'Готовая команда',
text:'Наши отделы уже собраны и сработаны - у нас есть опыт с компаниями разных маштабов, благодаря чему мы подключимся к работе всего за несколько дней.'},
pro2:{title:'Высокая мотивация',
text:'Мы получаем процент от вашей выручки. Так как для нас оплата целиком зависит от результата, мы замотивированы помогать вашей компании расти.'},
pro3:{title:'Опыт и экспертиза',
text:'Проекты, которые мы ведем, растут в десятки раз. Для сохранения таких показателей, мы проходим сертификации и проходим дополнительное обучение.'},
pro4:{title:'Персональный менеджер',
text:'Менеджер всегда на cвязи в течение рабочего дня и ответит на любые вопросы, связанные с нашей работой.'},
};
const modal = document.getElementById('modal'),
mEyebrow = document.getElementById('mEyebrow'),
mTitle = document.getElementById('mTitle'),
mText = document.getElementById('mText'),
mList = document.getElementById('mList');
function openModal(key){
const d = MODALS[key];
if(!d || !modal) return;
mEyebrow.textContent = d.eyebrow || 'MPULSE'; /* было undefined */
mTitle.textContent = d.title;
mText.textContent = d.text;
mList.innerHTML = (d.list || []).map(i => '<li>' + i + '</li>').join('');
mList.style.display = (d.list && d.list.length) ? '' : 'none';
modal.classList.add('open');
modal.setAttribute('aria-hidden','false');
}
function closeModal(){ if(!modal) return; modal.classList.remove('open'); modal.setAttribute('aria-hidden','true'); }
document.querySelectorAll('[data-modal]').forEach(el=> el.addEventListener('click', ()=> openModal(el.dataset.modal)));
document.querySelectorAll('[data-close]').forEach(el=> el.addEventListener('click', closeModal));
addEventListener('keydown', e=>{ if(e.key === 'Escape') closeModal(); });
/* ===========================================================================
4. ГРАФИКИ КЕЙСОВ (млн ₽)
Чёрным акцентом помечается только столбец начала работы (класс .is-start),
все остальные столбцы — зелёные.
=========================================================================== */
const months = ['янв','фев','мар','апр','май','июн','июл','авг','сен','окт','ноя','дек'];
const CHARTS = {
zu:{ start:'2024-0', mark:5, markText:'MPULSE · июнь 2024', v:[
0.73,0.74,1.04,0.67,0.64,0.88,1.89,2.43,3.27,4.05,3.59,6.70,
6.49,8.92,10.21,11.02,13.40,11.89,20.08,24.11,41.67,25.30,25.35,25.17,
18.14,31.78,40.33,26.32,30.08,57.35]},
vesna:{ start:'2024-0', mark:6, markText:'MPULSE · июль 2024', v:[
2.18,2.78,2.18,2.49,1.83,1.74,2.84,2.93,3.28,3.02,2.49,4.24,
2.95,3.78,5.19,3.75,4.69,4.40,7.21,7.13,13.96,9.26,8.83,10.70,
14.20,23.35,19.12,9.51,7.76,13.75,18.92]}
};
function renderChart(el){
const set = CHARTS[el.dataset.chart];
if(!set) return;
const max = Math.max(...set.v);
let [y,m] = set.start.split('-').map(Number);
set.v.forEach((v,i)=>{
const isStart = set.mark === i;
const bar = document.createElement('div');
/* .is-max больше не красит столбец в чёрный — акцент только у .is-start */
bar.className = 'bar' + (v === max ? ' is-max' : '') + (isStart ? ' is-start' : '');
bar.style.height = '0%';
bar.innerHTML =
'<span class="tip">'+months[m]+' '+y+' · '+v.toFixed(2)+' млн ₽</span>' +
(isStart ? '<span class="mark">'+(set.markText||'')+'</span>' : '');
el.appendChild(bar);
const h = (v/max*100);
requestAnimationFrame(()=>{ bar.style.height = h+'%'; });
m++; if(m>11){ m=0; y++; }
});
}
document.querySelectorAll('.chart').forEach(c=>{
new IntersectionObserver(es=>es.forEach(e=>{
if(e.isIntersecting && !e.target.dataset.done){ e.target.dataset.done=1; renderChart(e.target); }
}),{threshold:.2}).observe(c);
});
/* ---------- 5. Reveal-анимации ---------- */
const io = new IntersectionObserver(es=>es.forEach(e=>{
if(!e.isIntersecting) return;
const el = e.target;
if(el.dataset.rv === 'stagger'){
el.classList.add('in');
[...el.children].forEach((c,i)=> c.style.transitionDelay = (i*90)+'ms');
} else {
setTimeout(()=> el.classList.add('in'), parseInt(getComputedStyle(el).getPropertyValue('--d')) || 0);
}
io.unobserve(el);
}),{threshold:.15});
document.querySelectorAll('[data-rv]').forEach(el=> io.observe(el));
/* ---------- Счётчики ---------- */
const cio = new IntersectionObserver(es=>es.forEach(e=>{
if(!e.isIntersecting) return;
const el = e.target, to = +el.dataset.count; let n = 0;
const step = Math.max(1, Math.round(to/40));
const t = setInterval(()=>{ n += step; if(n >= to){ n = to; clearInterval(t); } el.textContent = n; },24);
cio.unobserve(el);
}),{threshold:.6});
document.querySelectorAll('[data-count]').forEach(el=> cio.observe(el));
/* Блок FAQ удалён со страницы — соответствующий обработчик убран,
чтобы не оставлять «мёртвый» код и обращения к .faq-a (null). */