/* Витрина кейсов «Реальный репетитор» — логика. Требует, чтобы ДО этого файла подключались hero.js и cases.js. */ (function(){ "use strict"; const INCOME_BANDS=[ {key:"zero", label:"с нуля", test:v=>v===0}, {key:"lt50", label:"до 50 т.р.", test:v=>v>0&&v<50000}, {key:"50_150", label:"50–150 т.р.", test:v=>v>=50000&&v<150000}, {key:"150_300",label:"150–300 т.р.", test:v=>v>=150000&&v<300000}, {key:"gt300", label:"300+ т.р.", test:v=>v>=300000} ]; // предметы: у кейса массив c.subjects, группа = сам предмет, редкие языки — в «Другие языки» const LANG_OTHER=new Set(["Китайский","Корейский","Японский","Немецкий","Французский", "Испанский","Арабский","Турецкий","Армянский","Итальянский","Татарский"]); const MISC=new Set(["Вокал","Рисование","Обучение письму","Психология","Финансовая грамотность"]); const SUBJ_ORDER=["Математика","Английский","Русский язык","Информатика","Физика", "Обществознание","История","Химия","Биология","Начальная школа","География", "Логопедия","Другие языки","Другое"]; function groupsOf(c){ const g=new Set(); (c.subjects||[]).forEach(s=>{ if(LANG_OTHER.has(s)) g.add("Другие языки"); else if(MISC.has(s)) g.add("Другое"); else g.add(s); }); if(!g.size) g.add("Другое"); return [...g]; } const audLabel=c=>c.audience?c.audience:null; const PAGE=24; let shown=PAGE; // поисковая строка кейса function haystack(c){ if(c._hay) return c._hay; const parts=[c.name,c.subject,c.subjectFull,(c.subjects||[]).join(" "),c.audience,c.status,c.life,c.cohort,c.hook, (c.pains||[]).join(" "),(c.blockers||[]).join(" "), (c.did||[]).map(d=>d[0]+" "+d[1]).join(" "),(c.changes||[]).join(" "), c.next,c.payback,c.fear,c.quote&&c.quote.t,c.a.format,c.b.format,c.a.src,c.b.src]; return c._hay=parts.filter(Boolean).join(" ").toLowerCase().replace(/ё/g,"е"); } const state={subject:new Set(),income:new Set(),pain:new Set(),life:new Set(),sort:"best",q:""}; const fmt=n=>new Intl.NumberFormat("ru-RU").format(Math.round(n)); const num=n=>String(n).replace(".",","); const tr=n=>n===null?"—":(n===0?"0":num(Math.round(n/100)/10).replace(",0","")+" т.р."); const startText=c=>c.a.income===0?"с нуля":tr(c.a.income); const money=n=>(n===null||n===undefined)?"—":fmt(n)+" ₽"; const perHour=(inc,h)=>h?inc/(h*4.33):null; function ratio(c){ if(c.a.rate&&c.b.rate) return {v:c.b.rate/c.a.rate,label:"стоимость часа",calc:false,a:c.a.rate,b:c.b.rate}; const pa=perHour(c.a.income,c.a.hours), pb=perHour(c.b.income,c.b.hours); if(pa&&pb&&pa>0) return {v:pb/pa,label:"отдача часа",calc:true,a:pa,b:pb}; return null; } function ratioText(r){ const pre=r.calc?"≈ ":""; return r.v>10 ? `${pre}${r.label} ${fmt(r.a)} → ${fmt(r.b)} ₽` : `${pre}${r.label} ×${num(r.v.toFixed(1))}`; } const isTime=c=>c.b.income<=c.a.income*1.05 && c.a.hours && c.b.hours && c.b.hoursw[0]).join("").toUpperCase();} function avatar(c,cls){ if(c.photo) return `${c.name}`; let h=0; for(const ch of c.name) h=(h*31+ch.charCodeAt(0))>>>0; const [bg,fg]=AVA_COLORS[h%AVA_COLORS.length]; return ``; } function uniq(get){const s=new Set();CASES.forEach(c=>[].concat(get(c)).forEach(v=>v&&s.add(v)));return [...s];} function buildChips(host,values,bucket){ host.innerHTML=""; values.forEach(({key,label})=>{ const b=document.createElement("button"); b.type="button";b.className="chip";b.textContent=label;b.setAttribute("aria-pressed","false"); b.onclick=()=>{state[bucket].has(key)?state[bucket].delete(key):state[bucket].add(key); b.setAttribute("aria-pressed",String(state[bucket].has(key)));shown=PAGE;render();}; host.appendChild(b); }); } function matches(c){ if(state.q&&!haystack(c).includes(state.q))return false; if(state.subject.size&&!groupsOf(c).some(g=>state.subject.has(g)))return false; if(state.life.size&&!state.life.has(c.life))return false; if(state.pain.size&&!c.pains.some(p=>state.pain.has(p)))return false; if(state.income.size){const b=INCOME_BANDS.find(b=>b.test(c.a.income));if(!b||!state.income.has(b.key))return false;} return true; } const SORTS={ best:(a,b)=>(b.strength||0)-(a.strength||0), growth:(a,b)=>(b.b.income-b.a.income)-(a.b.income-a.a.income), hourly:(a,b)=>((ratio(b)||{v:0}).v)-((ratio(a)||{v:0}).v), hours:(a,b)=>((b.a.hours||0)-(b.b.hours||b.a.hours||0))-((a.a.hours||0)-(a.b.hours||a.a.hours||0)), income:(a,b)=>b.b.income-a.b.income, lowest:(a,b)=>a.a.income-b.a.income }; function tile(c){ const el=document.createElement("button");el.type="button";el.className="tile"; const r=ratio(c); const tags=[]; if(r) tags.push(`${ratioText(r)}`); if(c.a.hours&&c.b.hours) tags.push(`часы ${num(c.a.hours)} → ${num(c.b.hours)}`); tags.push(`${c.status}`); const money = isTime(c) ? `
Те же ${tr(c.b.income)} за ${num(c.b.hours)} ч в неделю вместо ${num(c.a.hours)}
` : `
${startText(c)}${tr(c.b.income)}
`; el.innerHTML=`
${avatar(c)}

${c.name}

${(c.subject||c.audience)?`${[c.subject,audLabel(c)].filter(Boolean).join(" · ")}`:""}
${money}

${c.hook}

${tags.join("")}
Читать историю
`; el.onclick=()=>openSheet(c); return el; } function activeCount(){return state.subject.size+state.income.size+state.pain.size+state.life.size;} function render(){ const list=CASES.filter(matches).sort(SORTS[state.sort]); const g=document.getElementById("grid");g.innerHTML=""; const sm=document.getElementById("showmore");sm.innerHTML=""; if(!list.length){ g.innerHTML='

По этому запросу историй не нашлось. Попробуйте снять фильтр или изменить слово в поиске.

'; }else{ list.slice(0,shown).forEach(c=>g.appendChild(tile(c))); if(list.length>shown){ const b=document.createElement("button"); b.type="button"; b.textContent=`Показать ещё ${Math.min(PAGE,list.length-shown)}`; b.onclick=()=>{shown+=PAGE;render();}; sm.appendChild(b); } } const n=list.length; const w=n%10===1&&n%100!==11?"история":(n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?"истории":"историй"); document.getElementById("count").textContent=`${n} ${w} из ${CASES.length}`; const ac=activeCount(); document.getElementById("fcount").textContent=ac?`(${ac})`:""; } function row(dt,dd,na){return `
${dt}
${dd}
`;} function side(p,title,b){ return `

${title}

${p.income===0?"0 ₽":money(p.income)}
${row("Часов в неделю", p.hours!==null&&p.hours!==undefined?num(p.hours):"—", !p.hours)} ${row("Стоимость часа", money(p.rate), !p.rate)} ${row("Учеников", p.students!==null&&p.students!==undefined?p.students:"—", p.students===null||p.students===undefined)} ${p.format?row("Формат", p.format):""} ${p.src?row("Ученики", p.src):""}
`; } function openSheet(c){ const r=ratio(c); const head = isTime(c) ? `
Те же ${money(c.b.income)} в месяц за ${num(c.b.hours)} ч в неделю вместо ${num(c.a.hours)}
` : `
${startText(c)}${tr(c.b.income)}
`; document.getElementById("sheet").innerHTML=`
${avatar(c,"ava-lg")}

${c.name}

${[c.subjectFull,audLabel(c),c.status].filter(Boolean).join(" · ")}
${head} ${r?`
${ratioText(r)}
`:""}
${side(c.a,"Было")}${side(c.b,"Стало",true)}

Было

${c.hook}

${c.fear?`
${c.fear}
`:""}

Что мешало

Что сделал

Рост дал не один инструмент, а их совокупность.

${c.payback?`

Что окупило программу

${c.payback}

`:""} ${c.next?`

Что дальше

${c.next}

`:""} ${c.changes?`

Как изменилась жизнь

`:""} ${c.quote?`

${c.quote.t}

${c.name}
`:""} ${c.proofs?`

Пруфы

${c.proofs.map(p=>`Скриншот`).join("")}
`:""}`; const s=document.getElementById("scrim");s.hidden=false; const btn=document.getElementById("close");btn.onclick=closeSheet;btn.focus(); } function closeSheet(){document.getElementById("scrim").hidden=true;} document.getElementById("scrim").onclick=e=>{if(e.target.id==="scrim")closeSheet();}; document.addEventListener("keydown",e=>{if(e.key==="Escape")closeSheet();}); document.getElementById("sort").onchange=e=>{state.sort=e.target.value;shown=PAGE;render();}; const qEl=document.getElementById("q"); let qt; qEl.oninput=e=>{clearTimeout(qt);qt=setTimeout(()=>{ state.q=e.target.value.trim().toLowerCase().replace(/ё/g,"е");shown=PAGE;render();},160);}; document.getElementById("qclr").onclick=()=>{qEl.value="";state.q="";shown=PAGE;render();qEl.focus();}; const fb=document.getElementById("fbtn"); fb.onclick=()=>{const f=document.querySelector(".filters"); const open=f.classList.toggle("open");fb.setAttribute("aria-expanded",String(open));}; document.getElementById("reset").onclick=()=>{ state.subject.clear();state.income.clear();state.pain.clear();state.life.clear(); document.querySelectorAll(".chip").forEach(c=>c.setAttribute("aria-pressed","false")); qEl.value="";state.q="";shown=PAGE;render();}; const subjCount={};CASES.forEach(c=>groupsOf(c).forEach(g=>subjCount[g]=(subjCount[g]||0)+1)); const subjChips=Object.keys(subjCount).sort((a,b)=>{ const ia=SUBJ_ORDER.indexOf(a), ib=SUBJ_ORDER.indexOf(b); if(ia>=0&&ib>=0) return ia-ib; if(ia>=0) return -1; if(ib>=0) return 1; return subjCount[b]-subjCount[a]; }).map(v=>({key:v,label:v})); buildChips(document.getElementById("f-subject"),subjChips,"subject"); buildChips(document.getElementById("f-income"),INCOME_BANDS.map(b=>({key:b.key,label:b.label})),"income"); buildChips(document.getElementById("f-pain"),uniq(c=>c.pains).map(v=>({key:v,label:v})),"pain"); buildChips(document.getElementById("f-life"),uniq(c=>c.life).map(v=>({key:v,label:v})),"life"); render(); // ---------- мозаика с фотографиями выпускников ---------- (function mosaic(){ const host=document.getElementById("mosaic"); if(!host) return; const card=host.querySelector(".herocard"); const ph=window.HERO_PHOTOS||[]; if(!ph.length) return; const cols=()=>{const w=innerWidth; return w<=760?4:(w<=1100?6:8);}; function build(){ host.querySelectorAll(".mtile").forEach(n=>n.remove()); const C=cols(); // сколько плиток над карточкой, по бокам и под ней const top=C, sides=(C<=4?0:(C-4)*2), rest=ph.length-top-sides; const mk=(src,wide)=>{ const d=document.createElement("div"); d.className="mtile"+(wide?" wide":""); const i=document.createElement("img"); i.src=src; i.alt=""; i.loading="lazy"; d.appendChild(i); return d; }; let k=0; const before=document.createDocumentFragment(), after=document.createDocumentFragment(); for(let i=0;i0){ const gaps=(C-(left%C||C))%C; // сколько ячеек не хватает let wide=Math.min(gaps,left); for(let i=0;i{clearTimeout(t);t=setTimeout(build,200);}); })(); })();