Files
Yuriy PanovandClaude Opus 5 d13feb7df8 fitnes: add the Fitness v2 mockup as the design reference
legacy/Fitnes v2/ is the static mockup the landing was just ported from
(README points to it as the current visual reference). launch.json gains a
fitnes-v2-legacy entry that serves it on :8090 for side-by-side review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:38:10 +06:00

162 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const FORM_ENDPOINT = '/api/leads/fitness-centers';
const devices = {
therapy: {name:'ЭкзоТерапия', role:'флагманский full-body recovery и premium-продукт', benefits:['заметный якорь зоны','восстановительный этап после нагрузки','повышает технологичность сервиса']},
magnet: {name:'ЭкзоМагнит 3 в 1', role:'medical-core для нейромышечных и опорно-двигательных маршрутов', benefits:['магнит + УВТ + инфракрасный модуль','работа по медицинскому протоколу','ядро для спортивной медицины']},
tecar: {name:'ЭкзоТекар 3 в 1', role:'активная локальная работа с тканями, фасциями и подвижностью', benefits:['TECAR + УВТ + ультразвук','инструмент специалиста','сильная связь с PT и return-to-sport']},
press: {name:'ЭкзоПресс', role:'потоковый recovery для ног после бега, leg day и нагрузки', benefits:['компрессия + тепло / холод','понятный сервис для массового спроса','легко включается в пакеты']},
hydrogen: {name:'ЭкзоВодород', role:'автономная lounge-процедура для recovery и recharge', benefits:['спокойный формат в кресле','минимальное участие оператора','отдельная причина прийти в recovery-day']}
};
const presets = {
recovery: {ids:['therapy','press','hydrogen'], title:'Recovery Start', subtitle:'Компактный вход в recovery с флагманской, потоковой и автономной процедурой.'},
sport: {ids:['magnet','tecar','press'], title:'Sport Core', subtitle:'Активное ядро для клуба с сильным PT, беговым, силовым или спортивным направлением.'},
premium: {ids:['therapy','tecar','hydrogen'], title:'Premium Mix', subtitle:'Статусная конфигурация с индивидуальной процедурой, флагманским recovery и lounge-сценарием.'}
};
let mode = 3;
let selected = new Set(presets.recovery.ids);
let currentPreset = 'recovery';
const threeImage = "assets/asset_09_cfa0f6440fb6.webp";
const fiveImage = "assets/asset_14_c3b984d644de.webp";
const cards = [...document.querySelectorAll('.device-card')];
const modeButtons = [...document.querySelectorAll('.mode-btn')];
const presetButtons = [...document.querySelectorAll('.preset-btn')];
function configString() { return [...selected].map(id => devices[id].name).join(', '); }
function uniqueBenefits(ids) {
const items = [];
ids.forEach(id => devices[id].benefits.forEach(x => { if(!items.includes(x)) items.push(x); }));
return items.slice(0,4);
}
function updateConstructor() {
const ids = [...selected];
cards.forEach(card => {
const active = selected.has(card.dataset.device);
card.classList.toggle('selected',active);
card.classList.toggle('locked',mode===5);
card.setAttribute('aria-pressed',String(active));
});
modeButtons.forEach(btn => btn.classList.toggle('active',Number(btn.dataset.mode)===mode));
document.getElementById('selectedCount').textContent=ids.length;
document.getElementById('modeCount').textContent=mode;
document.getElementById('pickerHint').textContent = mode===5 ? 'Полная линейка выбрана' : (ids.length===3 ? 'Конфигурация собрана' : `Выберите ещё ${3-ids.length}`);
document.getElementById('presetRow').style.display = mode===3 ? 'flex':'none';
presetButtons.forEach(btn=>btn.classList.toggle('active',btn.dataset.preset===currentPreset && mode===3));
const image = document.getElementById('configImage');
image.style.opacity='0';
setTimeout(()=>{ image.src=mode===5?fiveImage:threeImage; image.alt=mode===5?'Полная зона восстановления с пятью аппаратами':'Пример зоны восстановления с тремя аппаратами'; image.style.opacity='1'; },180);
document.getElementById('configTag').textContent=mode===5?'Полная EXO-платформа':'Конструктор на 3 аппарата';
document.getElementById('configNumber').textContent=mode===5?'05':String(ids.length).padStart(2,'0');
const exactPreset = Object.entries(presets).find(([_,p])=>p.ids.length===ids.length && p.ids.every(id=>selected.has(id)));
const incomplete = mode===3 && ids.length<3;
const title = mode===5 ? 'Полная зона — 5 аппаратов' : (incomplete ? `Выбрано ${ids.length} из 3 аппаратов` : (exactPreset ? `${exactPreset[1].title} — 3 аппарата` : 'Персональный микс — 3 аппарата'));
const subtitle = mode===5 ? 'Полный маршрут: active medical-core, premium recovery, потоковые и автономные процедуры.' : (incomplete ? 'Можно продолжить подбор или получить консультацию — специалист поможет собрать оптимальную конфигурацию.' : (exactPreset ? exactPreset[1].subtitle : 'Состав собран под выбранные вами приоритеты. Итоговая логика уточняется после аудита клуба.'));
document.getElementById('configTitle').textContent=title;
document.getElementById('configSubtitle').textContent=subtitle;
document.getElementById('selectedList').innerHTML=ids.map(id=>`<span class="selected-pill">${devices[id].name}</span>`).join('');
document.getElementById('configBenefits').innerHTML=uniqueBenefits(ids).map(x=>`<div class="config-benefit"><i><svg viewBox='0 0 24 24' aria-hidden='true'><path d='m5 12 4 4L19 6' fill='none' stroke='currentColor' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/></svg></i><span>${x}</span></div>`).join('');
document.getElementById('configurationInput').value=ids.length ? `${ids.length} аппарата: ${configString()}` : 'Конфигурация не выбрана — нужна консультация';
const cta=document.getElementById('configCta');
cta.style.opacity='1';cta.style.pointerEvents='auto';
}
cards.forEach(card=>card.addEventListener('click',()=>{
if(mode===5) return;
const id=card.dataset.device;
currentPreset='';
if(selected.has(id)) selected.delete(id);
else { if(selected.size>=3) selected.delete([...selected][0]); selected.add(id); }
updateConstructor();
}));
modeButtons.forEach(btn=>btn.addEventListener('click',()=>{
mode=Number(btn.dataset.mode);
if(mode===5){ selected=new Set(Object.keys(devices)); currentPreset=''; }
else { selected=new Set(presets.recovery.ids); currentPreset='recovery'; }
updateConstructor();
}));
presetButtons.forEach(btn=>btn.addEventListener('click',()=>{
mode=3;currentPreset=btn.dataset.preset;selected=new Set(presets[currentPreset].ids);updateConstructor();
}));
document.getElementById('resetConfig').addEventListener('click',()=>{mode=3;currentPreset='recovery';selected=new Set(presets.recovery.ids);updateConstructor();});
document.getElementById('configCta').addEventListener('click',()=>{const picked=configString();document.getElementById('comment').value=picked?`Интересует конфигурация: ${selected.size} аппарата — ${picked}`:'Нужна консультация по подбору конфигурации Recovery Zone';});
updateConstructor();
const header=document.getElementById('header');
const mobileCta=document.getElementById('mobileCta');
function onScroll(){header.classList.toggle('scrolled',scrollY>35);mobileCta.classList.toggle('show',scrollY>window.innerHeight*.75 && scrollY<document.body.scrollHeight-window.innerHeight*1.25)}
addEventListener('scroll',onScroll,{passive:true});onScroll();
const observer=new IntersectionObserver(entries=>entries.forEach(entry=>{if(entry.isIntersecting){entry.target.classList.add('is-visible');observer.unobserve(entry.target)}}),{threshold:.12});
document.querySelectorAll('.reveal,.market-panel').forEach(el=>observer.observe(el));
const modal=document.getElementById('imageModal'), modalImg=document.getElementById('modalImage');
document.querySelectorAll('.gallery-card').forEach(card=>card.addEventListener('click',()=>{modalImg.src=card.dataset.full;modal.classList.add('open');document.body.classList.add('locked')}));
function closeModal(){modal.classList.remove('open');document.body.classList.remove('locked')}
modal.querySelector('.modal-close').addEventListener('click',closeModal);modal.addEventListener('click',e=>{if(e.target===modal)closeModal()});addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
function collectUtm(){const p=new URLSearchParams(location.search),o={};['utm_source','utm_medium','utm_campaign','utm_content','utm_term'].forEach(k=>{if(p.get(k))o[k]=p.get(k)});return o}
async function handleLead(event){
event.preventDefault();
const form=event.currentTarget,status=document.getElementById('formStatus'),btn=document.getElementById('submitBtn');
if(!form.checkValidity()){form.reportValidity();status.textContent='Проверьте обязательные поля и согласие.';return false}
if(form.website.value) return false;
const payload=Object.fromEntries(new FormData(form).entries());Object.assign(payload,collectUtm(),{page:'/fitness',created_at:new Date().toISOString()});
btn.disabled=true;btn.textContent='Отправляем…';status.textContent='';
try{
const res=await fetch(FORM_ENDPOINT,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
if(!res.ok)throw new Error('HTTP '+res.status);
status.textContent='Спасибо. Заявка отправлена — специалист свяжется с вами.';status.style.color='#087F73';form.reset();updateConstructor();
window.dataLayer=window.dataLayer||[];window.dataLayer.push({event:'fitness_lead_sent',configuration:payload.configuration});
}catch(error){
localStorage.setItem('exo_fitness_lead_draft',JSON.stringify(payload));
status.textContent='Не удалось отправить форму автоматически. Данные сохранены в браузере — свяжитесь с нами по +7 939 717-80-80.';status.style.color='#A14D12';
console.warn('Lead endpoint error',error,payload);
}finally{btn.disabled=false;btn.textContent='Получить консультацию →';}
return false;
}
window.handleLead=handleLead;
(function(){
const money = new Intl.NumberFormat('ru-RU',{maximumFractionDigits:0});
const val = (root, selector) => {
const el=root.querySelector(selector); if(!el) return 0;
return Number(String(el.value||'').replace(/\s/g,'').replace(',','.'))||0;
};
document.querySelectorAll('[data-econ-calc]').forEach(root=>{
const type=root.getAttribute('data-econ-calc');
const update=()=>{
let month=0, volume=0, ready=false;
if(type==='hotel'){
const programs=val(root,'#hotelPrograms'), check=val(root,'#hotelCheck'), days=val(root,'#hotelDays');
ready=programs>0&&check>0&&days>0; volume=programs*days; month=volume*check;
}else{
const programs=val(root,'#fitPrograms'), check=val(root,'#fitCheck'), days=val(root,'#fitDays');
ready=programs>0&&check>0&&days>0; volume=programs*days; month=volume*check;
}
const monthEl=root.querySelector('[data-econ-month]'), yearEl=root.querySelector('[data-econ-year]'), volumeEl=root.querySelector('[data-econ-volume]');
const paybackEl=root.querySelector('[data-econ-payback]'), roiEl=root.querySelector('[data-econ-roi]');
if(!ready){
if(monthEl) monthEl.textContent='—';
if(yearEl) yearEl.textContent='—';
if(volumeEl) volumeEl.textContent=type==='hotel'?'Введите программы и средний чек':'Введите программы в день и средний чек';
if(paybackEl) paybackEl.textContent='—';
if(roiEl) roiEl.textContent='—';
return;
}
if(monthEl) monthEl.textContent=money.format(month)+' ₽';
if(yearEl) yearEl.textContent=money.format(month*12)+' ₽';
if(volumeEl) volumeEl.textContent=type==='hotel'?money.format(volume)+' программ в месяц':money.format(val(root,'#fitPrograms'))+' × '+money.format(val(root,'#fitDays'))+' = '+money.format(volume)+' программ в месяц';
if(type==='fitness' && paybackEl && roiEl){
const investment=val(root,'#fitInvestment');
if(investment>0 && month>0){
const payback=investment/month;
const roi=((month*12-investment)/investment)*100;
paybackEl.textContent=(Math.round(payback*10)/10).toLocaleString('ru-RU')+' мес.';
roiEl.textContent=Math.round(roi).toLocaleString('ru-RU')+'%';
}else{
paybackEl.textContent='Укажите инвестиции';
roiEl.textContent='Укажите инвестиции';
}
}
};
root.querySelectorAll('input').forEach(input=>input.addEventListener('input',update)); update();
});
})();