/* ===== Подари вторую жизнь — Catalog + Filter (подбор) ===== */ const emptyFilter = () => ({ species:'all', sizes:[], ages:[], sexes:[], colors:[], breed:'' }); function filterMatch(a, f){ if(f.species!=='all' && a.species!==f.species) return false; if(f.sizes.length && !f.sizes.includes(a.size)) return false; if(f.ages.length && !f.ages.includes(a.ageGroup)) return false; if(f.sexes.length && !f.sexes.includes(a.sex)) return false; if(f.colors.length && !f.colors.includes(a.color)) return false; if(f.breed && a.breed!==f.breed) return false; return true; } const toggleIn = (arr, v) => arr.includes(v) ? arr.filter(x=>x!==v) : [...arr, v]; /* ---- reusable filter controls ---- */ function FilterControls({ f, setF }){ const breeds = useMemo(()=>{ const set = PF.animals.filter(a=> f.species==='all'||a.species===f.species).map(a=>a.breed); return [...new Set(set)].sort(); },[f.species]); const Field = ({label, children}) => (
{label}
{children}
); return (
{PF.SIZES.map(s=>( ))}
{PF.AGES.map(s=>( ))}
{PF.SEX.map(s=>( ))}
{PF.COLORS.map(c=>( ))}
); } /* ---- Results grid with "показать ещё" ---- */ function ResultsGrid({ list, loading, page=8, onReset }){ const [shown, setShown] = useState(page); useEffect(()=>{ setShown(page); },[list, page]); if(loading) return ; if(!list.length) return ( Сбросить фильтры}/> ); const vis = list.slice(0, shown); return ( <>
{vis.map((a,i)=>)}
{shown < list.length &&
} ); } /* ---- Catalog screen ---- */ function CatalogScreen(){ const { route } = useNav(); const [f, setF] = useState(()=> ({...emptyFilter(), species: route.params?.species || 'all'})); const [loading, setLoading] = useState(true); const [openF, setOpenF] = useState(false); useEffect(()=>{ const t=setTimeout(()=>setLoading(false),700); return ()=>clearTimeout(t); },[]); const list = useMemo(()=> PF.animals.filter(a=>filterMatch(a,f)), [f]); const active = f.sizes.length+f.ages.length+f.sexes.length+f.colors.length+(f.breed?1:0)+(f.species!=='all'?1:0); return (

Все животные

{loading?'Загружаем подопечных…':`Найдено ${list.length} ${plural(list.length,'друг','друга','друзей')}, готовых обрести дом`}

setF(emptyFilter())}/>
{openF && setOpenF(false)} count={list.length}/>}
); } /* ---- Filter (Подбор) screen — centered guided form ---- */ /* ---- Подбор друга — пошаговый квиз ---- */ const QUIZ = [ { key:'species', title:'Кого вы ищете?', sub:'С кем вам будет уютнее всего', cols:3, opts:[ {id:'cat', label:'Кошку', Ico:Icons.cat, hint:'мягкая и независимая'}, {id:'dog', label:'Собаку', Ico:Icons.dog, hint:'верный компаньон'}, {id:'all', label:'Любого', Ico:Icons.pawHeart, hint:'покажите всех'}, ]}, { key:'size', title:'Какого размера друг?', sub:'Подумайте о своём доме и ритме жизни', cols:2, opts:[ {id:'small', label:'Небольшой', Ico:Icons.pawHeart, sc:0.7, hint:'до 5 кг'}, {id:'medium', label:'Средний', Ico:Icons.pawHeart, sc:0.9, hint:'5–15 кг'}, {id:'large', label:'Крупный', Ico:Icons.pawHeart, sc:1.1, hint:'от 15 кг'}, {id:'any', label:'Неважно', Ico:Icons.check, hint:'любой размер'}, ]}, { key:'age', title:'Какой возраст ближе?', sub:'У каждого возраста своё очарование', cols:2, opts:[ {id:'baby', label:'Малыш', Ico:Icons.cake, hint:'до 1 года'}, {id:'young', label:'Молодой', Ico:Icons.cake, hint:'1–3 года'}, {id:'adult', label:'Взрослый', Ico:Icons.cake, hint:'3–7 лет'}, {id:'senior', label:'Постарше', Ico:Icons.heart, hint:'от 7 лет · подарите второй шанс'}, ]}, { key:'sex', title:'Пол имеет значение?', sub:'Можно довериться сердцу и выбрать «неважно»', cols:3, opts:[ {id:'male', label:'Мальчик', Ico:Icons.male}, {id:'female', label:'Девочка', Ico:Icons.female}, {id:'any', label:'Неважно', Ico:Icons.pawHeart}, ]}, ]; function answersToFilter(ans){ return { species: ans.species && ans.species!=='all' ? ans.species : 'all', sizes: ans.size && ans.size!=='any' ? [ans.size] : [], ages: ans.age && ans.age!=='any' ? [ans.age] : [], sexes: ans.sex && ans.sex!=='any' ? [ans.sex] : [], colors:[], breed:'', }; } /* гибрид-поиск: разбор свободного описания на фасеты */ function parseDescription(text){ const s = (text||'').toLowerCase(); const f = { species:'all', sizes:[], ages:[], sexes:[], colors:[], breed:'' }; const chips = []; const add = (l)=>{ if(!chips.includes(l)) chips.push(l); }; if(/(кош|кот|кис|котён|котен)/.test(s)){ f.species='cat'; add('Кошка'); } else if(/(собак|пёс|пес|щен|псин|пёсик)/.test(s)){ f.species='dog'; add('Собака'); } if(/(маленьк|мелк|небольш|компактн|крошк|малютк)/.test(s)){ f.sizes.push('small'); add('Небольшой'); } if(/(средн)/.test(s)){ f.sizes.push('medium'); add('Средний'); } if(/(больш|крупн|огромн)/.test(s)){ f.sizes.push('large'); add('Крупный'); } if(/(котён|котен|щен|младен|новорож|до года|кроха)/.test(s)){ f.ages.push('baby'); add('Малыш'); } if(/(молод|подрост|юн)/.test(s)){ f.ages.push('young'); add('Молодой'); } if(/(взросл)/.test(s)){ f.ages.push('adult'); add('Взрослый'); } if(/(пожил|в возрасте|старш|седой|второй шанс)/.test(s)){ f.ages.push('senior'); add('Постарше'); } if(/(мальчик|кобел|самец)/.test(s)){ f.sexes.push('male'); add('Мальчик'); } if(/(девочк|кошечк|сучк|самк)/.test(s)){ f.sexes.push('female'); add('Девочка'); } [['чёрн|черн','black','Чёрный'],['бел','white','Белый'],['сер|дымч','grey','Серый'], ['рыж|оранж|апельсин','ginger','Рыжий'],['коричн|шоколад|бур','brown','Коричневый'], ['трёхцв|трехцв|черепах','tricolor','Трёхцветный'],['полос|табби|тигр','tabby','Полосатый']] .forEach(([re,id,lab])=>{ if(new RegExp(re).test(s)){ f.colors.push(id); add(lab); } }); return { filter:f, chips }; } const DESC_EXAMPLES = ['молодая рыжая кошка','небольшой спокойный пёс','чёрный котёнок','крупная собака-мальчик','пожилой кот']; function FilterScreen(){ const { go } = useNav(); const [mode, setMode] = useState('quiz'); // quiz | text const [step, setStep] = useState(0); const [ans, setAns] = useState({}); const [desc, setDesc] = useState(''); const [phase, setPhase] = useState('quiz'); // quiz | loading | result const [resultFilter, setResultFilter] = useState(null); const [chips, setChips] = useState([]); const total = QUIZ.length; const q = QUIZ[step]; const list = useMemo(()=> resultFilter ? PF.animals.filter(a=>filterMatch(a,resultFilter)) : [], [resultFilter]); const toResult = (filter, labelChips)=>{ setResultFilter(filter); setChips(labelChips); setPhase('loading'); setTimeout(()=>{ setPhase('result'); window.scrollTo({top:0,behavior:'instant'in document.body.style?'instant':'auto'}); }, 950); }; const choose = (optId)=>{ const nextAns = { ...ans, [q.key]: optId }; setAns(nextAns); setTimeout(()=>{ if(step < total-1) setStep(step+1); else { const labels = QUIZ.map(qq=>{ const o=qq.opts.find(x=>x.id===nextAns[qq.key]); return o&&!['all','any'].includes(o.id)?o.label:null; }).filter(Boolean); toResult(answersToFilter(nextAns), labels); } }, 230); }; const submitText = ()=>{ const { filter, chips:c } = parseDescription(desc); toResult(filter, c); }; const back = ()=> step>0 ? setStep(step-1) : null; const restart = ()=>{ setAns({}); setStep(0); setDesc(''); setResultFilter(null); setPhase('quiz'); window.scrollTo(0,0); }; // RESULT if(phase==='result'){ return (
Готово · подобрали для вас

{list.length>0?'Кажется, это ваши друзья':'Пока никто не подошёл'}

{chips.length>0 &&
{chips.map((c,i)=>{c})}
}
); } // LOADING if(phase==='loading'){ return (

Подбираем друзей…

Сверяем ваши пожелания с теми, кто ждёт дом

); } // QUIZ / TEXT const pct = Math.round((step/total)*100); return (
Подбор друга
{mode==='text' ?

Опишите друга мечты

Своими словами — мы поймём вид, размер, возраст, пол и окрас.