/* ===== App shell + router ===== */ const { useState:uS, useEffect:uE } = React; const DEFAULT_ROUTE = {n:'home', p:null}; const DEFAULT_USER = { name:'Айбек Усенов', email:'aibek.u@mail.kg', role:'user', cabinet_access:false, league_member:false }; const AUTH_CACHE_KEY = 'krsu_auth_user'; const LANG_CACHE_KEY = 'krsu_site_lang'; const STAFF_ROLES = ['admin','manager','editor','league_sport','league_infrastructure','league_partners','league_support']; const APP_BASE_PATH = (() => { const raw = window.__APP_BASE_PATH__ || '/'; if(raw === '/') return '/'; return `${String(raw).replace(/\/+$/,'')}/`; })(); function readPreferredLang(){ try{ return localStorage.getItem(LANG_CACHE_KEY)==='ky' ? 'ky' : 'ru'; }catch(e){ return 'ru'; } } function writePreferredLang(lang){ try{ localStorage.setItem(LANG_CACHE_KEY, lang==='ky' ? 'ky' : 'ru'); }catch(e){} } function setGoogleTranslateCookie(targetLang){ const value = `/ru/${targetLang}`; const host = location.hostname; const domain = host.includes('.') ? `.${host}` : host; document.cookie = `googtrans=${value}; path=/`; document.cookie = `googtrans=${value}; path=/; domain=${host}`; if(domain !== host) document.cookie = `googtrans=${value}; path=/; domain=${domain}`; } function clearGoogleTranslateCookie(){ const host = location.hostname; const domain = host.includes('.') ? `.${host}` : host; const expire = 'Thu, 01 Jan 1970 00:00:00 GMT'; [ `googtrans=; expires=${expire}; path=/`, `googtrans=; expires=${expire}; path=/; domain=${host}`, domain !== host ? `googtrans=; expires=${expire}; path=/; domain=${domain}` : '', ].filter(Boolean).forEach(cookie=>{ document.cookie = cookie; }); } function initGoogleTranslateElement(){ if(window.__googleTranslateInitialized) return true; if(!window.google?.translate?.TranslateElement) return false; const holder = document.getElementById('google_translate_element'); if(!holder) return false; holder.innerHTML = ''; new window.google.translate.TranslateElement({ pageLanguage:'ru', includedLanguages:'ru,ky', autoDisplay:false, }, 'google_translate_element'); window.__googleTranslateInitialized = true; return true; } function ensureGoogleTranslateScript(){ if(window.google?.translate?.TranslateElement){ initGoogleTranslateElement(); return Promise.resolve(); } if(window.__googleTranslatePromise) return window.__googleTranslatePromise; const promise = new Promise((resolve, reject)=>{ let settled = false; const finish = (error=null)=>{ if(settled) return; settled = true; clearTimeout(timeout); if(error) reject(error); else resolve(); }; const timeout = setTimeout(()=>finish(new Error('Google Translate не ответил вовремя')), 15000); window.googleTranslateElementInit = ()=>{ try{ if(initGoogleTranslateElement()) finish(); else finish(new Error('Не удалось создать переключатель Google Translate')); }catch(error){ finish(error); } }; const existing = document.querySelector('script[data-google-translate]'); if(existing) existing.remove(); const script = document.createElement('script'); script.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit'; script.async = true; script.dataset.googleTranslate = 'true'; script.onerror = ()=>finish(new Error('Не удалось загрузить Google Translate')); document.head.appendChild(script); }); window.__googleTranslatePromise = promise.catch(error=>{ window.__googleTranslatePromise = null; window.__googleTranslateInitialized = false; throw error; }); return window.__googleTranslatePromise; } function triggerGoogleLanguage(targetLang){ const combo = document.querySelector('.goog-te-combo'); if(!combo) return false; combo.value = targetLang; combo.dispatchEvent(new Event('change', { bubbles:true })); return true; } function applyGoogleTranslate(targetLang){ setGoogleTranslateCookie(targetLang); return ensureGoogleTranslateScript().then(()=>new Promise((resolve,reject)=>{ let attempts = 0; const timer = setInterval(()=>{ attempts += 1; if(triggerGoogleLanguage(targetLang)){ clearInterval(timer); resolve(); }else if(attempts > 40){ clearInterval(timer); reject(new Error('Переключатель Google Translate не появился')); } }, 250); })); } function readCachedAuthUser(){ try{ const raw = localStorage.getItem(AUTH_CACHE_KEY) || sessionStorage.getItem(AUTH_CACHE_KEY); if(!raw) return null; const parsed = JSON.parse(raw); if(parsed && parsed.email){ localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify(parsed)); sessionStorage.removeItem(AUTH_CACHE_KEY); } return parsed && parsed.email ? parsed : null; }catch(e){ return null; } } function writeCachedAuthUser(nextUser){ try{ if(nextUser && nextUser.email){ const cacheUser={ id:nextUser.id, name:nextUser.name, email:nextUser.email, role:nextUser.role, league_member:!!nextUser.league_member, cabinet_access:!!nextUser.cabinet_access, }; localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify(cacheUser)); } else localStorage.removeItem(AUTH_CACHE_KEY); sessionStorage.removeItem(AUTH_CACHE_KEY); }catch(e){} } function parseRouteString(rawValue=''){ const raw = String(rawValue || '').replace(/^#/, ''); if(!raw) return DEFAULT_ROUTE; const [name, ...rest] = raw.split('/'); return {n:name || 'home', p:rest.length ? decodeURIComponent(rest.join('/')) : null}; } function parseRouteFromHash(){ return parseRouteString(location.hash); } function parseRouteFromPathname(pathname=location.pathname){ let localPath = String(pathname || '/'); if(APP_BASE_PATH !== '/' && localPath.startsWith(APP_BASE_PATH.slice(0, -1))){ localPath = localPath.slice(APP_BASE_PATH.length - 1) || '/'; } const trimmed = localPath.replace(/^\/+|\/+$/g, ''); if(!trimmed) return DEFAULT_ROUTE; const parts = trimmed.split('/'); if(parts[0] === 'news' && parts[1]) return {n:'news-item', p:decodeURIComponent(parts.slice(1).join('/'))}; if(parts[0] === 'news') return {n:'news', p:null}; if(parts[0] === 'races' && parts[1]) return {n:'race-item', p:decodeURIComponent(parts.slice(1).join('/'))}; if(parts[0] === 'races') return {n:'races', p:null}; if(parts[0] === 'league') return {n:'league', p:null}; if(parts[0] === 'rating') return {n:'pilot-rating', p:null}; if(parts[0] === 'pilots' && parts[1]) return {n:'public-pilot', p:decodeURIComponent(parts.slice(1).join('/'))}; if(parts[0] === 'services' && parts[1]) return {n:'service-detail', p:decodeURIComponent(parts.slice(1).join('/'))}; if(parts[0] === 'services') return {n:'services', p:null}; if(parts[0] === 'trial-lesson') return {n:'trial-lesson', p:null}; if(parts[0] === 'autumn-school') return {n:'autumn-school', p:null}; if(parts[0] === 'courses' && parts[1]) return {n:'course', p:decodeURIComponent(parts.slice(1).join('/'))}; if(parts[0] === 'docs' && parts[1]) return {n:'page', p:decodeURIComponent(parts.slice(1).join('/'))}; return DEFAULT_ROUTE; } function parseRouteFromLocation(){ if(location.hash) return parseRouteFromHash(); return parseRouteFromPathname(location.pathname); } function buildHashFromRoute(name, param=null){ return param==null ? name : [name, ...String(param).split('/').map(part=>encodeURIComponent(part))].join('/'); } function isPathRoute(name){ return ['home','news','news-item','races','race-item','league','pilot-rating','public-pilot','services','service-detail','trial-lesson','autumn-school','course','page'].includes(name); } function buildUrlFromRoute(name, param=null){ const root = APP_BASE_PATH; if(name === 'home') return root; if(name === 'news') return `${root}news`; if(name === 'news-item') return `${root}news/${String(param || '').split('/').map(part=>encodeURIComponent(part)).join('/')}`; if(name === 'races') return `${root}races`; if(name === 'race-item') return `${root}races/${String(param || '').split('/').map(part=>encodeURIComponent(part)).join('/')}`; if(name === 'league') return `${root}league`; if(name === 'pilot-rating') return `${root}rating`; if(name === 'public-pilot') return `${root}pilots/${encodeURIComponent(String(param||''))}`; if(name === 'services') return `${root}services`; if(name === 'service-detail') return `${root}services/${encodeURIComponent(String(param||''))}`; if(name === 'trial-lesson') return `${root}trial-lesson`; if(name === 'autumn-school') return `${root}autumn-school`; if(name === 'course') return `${root}courses/${String(param || '').split('/').map(part=>encodeURIComponent(part)).join('/')}`; if(name === 'page') return `${root}docs/${String(param || '').split('/').map(part=>encodeURIComponent(part)).join('/')}`; return `${root}#${buildHashFromRoute(name, param)}`; } function isStudentAreaRoute(name){ return ['cabinet','cab-materials','cab-certs','cab-settings','player','test','payment', 'league-rating','league-infrastructure','league-discounts','league-pilot-card','league-support'].includes(name); } function isStaffRole(role){ return STAFF_ROLES.includes(String(role || '')); } function hasCabinetAccess(user){ return isStaffRole(user?.role) || !!user?.cabinet_access; } function isLeagueMemberUser(user){ return !!(user?.league_member || user?.leagueMember); } function ensureMetaTag(attrName, attrValue){ let tag = document.head.querySelector(`meta[${attrName}="${attrValue}"]`); if(!tag){ tag = document.createElement('meta'); tag.setAttribute(attrName, attrValue); document.head.appendChild(tag); } return tag; } function updateMetaTag(attrName, attrValue, content){ const tag = ensureMetaTag(attrName, attrValue); tag.setAttribute('content', content || ''); } function Header({ go, route, lang, setLang, onDark, loggedIn=false, user=null }){ const [scrolled,setScrolled]=uS(false); const [openM,setOpenM]=uS(false); uE(()=>{ const f=()=>setScrolled(window.scrollY>12); f(); window.addEventListener('scroll',f); return ()=>window.removeEventListener('scroll',f); },[]); const activeRoute = route==='news-item' ? 'news' : route==='race-item' || route==='pilot-rating' || route==='league' ? 'races' : route==='course' || route==='trial-lesson' || route==='autumn-school' ? 'education' : route==='service-detail' ? 'services' : route; const links=[['home','Главная'],['services','Услуги'],['education','Образование'],['races','Гонки'],['news','Новости'],['faq','FAQ'],['contacts','Контакты']]; const dark = onDark && !scrolled; const accountLabel = loggedIn ? (user?.name || user?.email || 'Мой кабинет') : 'Личный кабинет'; const accountRoute = loggedIn ? (isStaffRole(user?.role) ? 'admin' : 'cabinet') : 'login'; const openAccount = ()=>go(accountRoute); return (
go('home')}>
{openM && (
setOpenM(false)}>
e.stopPropagation()}>
{links.map(([k,t])=>())}
)}
); } function Footer({ go }){ return ( ); } function AwaitingApproval({ go, enroll=[], latestRejectedPayment=null }){ const pendingItem = enroll.find(item=>item.access==='pending'); const rejectedItem = !pendingItem && latestRejectedPayment ? latestRejectedPayment : null; const title = rejectedItem ? 'Оплата не подтверждена' : 'Оплата ещё не подтверждена'; const lead = rejectedItem ? 'Мы проверили заявку и пока не смогли подтвердить платёж. Ниже указали причину и оставили быстрый переход к повторной оплате.' : 'Как только администратор подтвердит оплату в панели управления, доступ в личный кабинет откроется автоматически.'; return (
Лаборатория БПЛА
{rejectedItem ? 'Статус оплаты' : 'Проверка заявки'}

{rejectedItem ? 'Платёж требует повторной проверки или новой отправки' : 'Мы получили вашу оплату и уже передали её на подтверждение'}

{rejectedItem ? 'Откройте оплату ещё раз, проверьте реквизиты и отправьте новую заявку. После подтверждения кабинет и материалы откроются автоматически.' : 'Обычно проверка занимает немного времени. Как только администратор подтвердит заявку, кабинет и материалы откроются автоматически.'}

Статус{rejectedItem ? 'Оплата отклонена' : 'Ожидает подтверждения'}
Следующий шаг{rejectedItem ? 'Проверить причину и повторить оплату' : 'Открытие доступа после проверки'}

{title}

{rejectedItem ? 'Оплата отклонена' : 'Ожидает подтверждения'} {pendingItem?.pendingDate && Заявка от {pendingItem.pendingDate}} {rejectedItem?.date && Проверено {rejectedItem.date}}

{lead}

{rejectedItem ? ( <>
Продукт{rejectedItem.courseTitle || 'Оплата курса'}
{rejectedItem.amount > 0 &&
Сумма{window.DATA?.fmt ? `${window.DATA.fmt(rejectedItem.amount)} сом` : `${rejectedItem.amount} сом`}
} {rejectedItem.reason &&
Причина отказа{rejectedItem.reason}
}
{rejectedItem.reason ? `Проверьте данные платежа и отправьте новую заявку с учётом этой причины: ${rejectedItem.reason}` : 'Проверьте данные платежа и отправьте новую заявку ещё раз. Если нужна помощь, свяжитесь с администрацией.'}
) : (
Заявка полученаМы сохранили вашу оплату в системе.
Идёт проверкаАдминистратор сверяет платёж и подтверждает доступ.
Кабинет откроетсяПосле подтверждения станут доступны курсы и материалы.
)} {!rejectedItem && ( <>
Ваша оплата на подтверждении. Ожидайте, пожалуйста. Пока можно вернуться на главную или открыть страницу оплаты, если нужно проверить заявку.

Мы автоматически проверяем статус оплаты каждые 30 секунд.

)}
{(pendingItem || rejectedItem) && }
); } const RACE_MONTHS_RU = { 'января':0, 'февраля':1, 'марта':2, 'апреля':3, 'мая':4, 'июня':5, 'июля':6, 'августа':7, 'сентября':8, 'октября':9, 'ноября':10, 'декабря':11, }; function raceExpiryTimestamp(value){ const source = String(value || '').trim().toLowerCase().replace(/ё/g, 'е'); if(!source) return null; let day; let month; let year; let hour; let minute; let match = source.match(/^(\d{1,2})\s+([а-я]+)\s+(\d{4})(?:\s*(?:г(?:ода)?\.?)?\s*(?:,|·|в)?\s*(\d{1,2}):(\d{2}))?/i); if(match && Object.prototype.hasOwnProperty.call(RACE_MONTHS_RU, match[2])){ day = Number(match[1]); month = RACE_MONTHS_RU[match[2]]; year = Number(match[3]); hour = match[4] == null ? null : Number(match[4]); minute = match[5] == null ? 0 : Number(match[5]); }else{ match = source.match(/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(?:[ t,]+(\d{1,2}):(\d{2}))?/); if(match){ day = Number(match[1]); month = Number(match[2]) - 1; year = Number(match[3]); hour = match[4] == null ? null : Number(match[4]); minute = match[5] == null ? 0 : Number(match[5]); }else{ match = source.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ t]+(\d{1,2}):(\d{2}))?/); if(!match) return null; year = Number(match[1]); month = Number(match[2]) - 1; day = Number(match[3]); hour = match[4] == null ? null : Number(match[4]); minute = match[5] == null ? 0 : Number(match[5]); } } const daysInMonth = year >= 1970 && month >= 0 && month <= 11 ? new Date(Date.UTC(year, month + 1, 0)).getUTCDate() : 0; if(!daysInMonth || day < 1 || day > daysInMonth || (hour != null && (hour < 0 || hour > 23)) || minute < 0 || minute > 59) return null; // A date without a time remains current through 23:59 in Bishkek (UTC+6). const localHour = hour == null ? 24 : hour; return Date.UTC(year, month, day, localHour, minute) - 6 * 60 * 60 * 1000; } function raceIsUpcoming(race, now=Date.now()){ if(race?.active === false) return false; const status = String(race?.status || '').trim().toLowerCase().replace(/ё/g, 'е'); if(/(заверш|оконч|прошел|отмен|не состо)/i.test(status)) return false; const expiresAt = raceExpiryTimestamp(race?.date); return expiresAt == null || expiresAt > now; } function Races({ go, loggedIn=false }){ const league = window.DATA?.leagueMembership || {}; const allRaces = DATA.races || []; const [raceClock,setRaceClock] = uS(()=>Date.now()); uE(()=>{ const timer = setInterval(()=>setRaceClock(Date.now()), 60000); return ()=>clearInterval(timer); },[]); const upcomingRaces = allRaces.filter(r=>raceIsUpcoming(r, raceClock)); const byDate = (a,b)=>(raceExpiryTimestamp(a.date) ?? Number.MAX_SAFE_INTEGER) - (raceExpiryTimestamp(b.date) ?? Number.MAX_SAFE_INTEGER); const pinnedRaces = upcomingRaces.filter(r=>r.pinned).sort(byDate); const regularRaces = upcomingRaces.filter(r=>!r.pinned).sort(byDate); const statusColor = s => s==='Регистрация открыта'||s==='Набор открыт' ? 'green' : s==='Скоро' ? 'amber' : 'gray'; return (
Дрон рейсинг

Дрон
рейсинг

Гонки дронов — спорт будущего

Анонсы соревнований и расписание. Вся основная информация, трансляции и результаты публикуются на наших каналах в социальных сетях.

Смотреть на YouTube Instagram
Рейтинг пилотов

Таблица лучших пилотов Лаборатории БПЛА и результаты последних стартов.

{league.title || 'Членство в Лиге'}

{league.subtitle || 'Платная регистрация для участия в Лиге Дронов и официальных заездах.'}

{window.DATA?.fmt ? `${window.DATA.fmt(Number(league.yearlyPrice || 8760))} сом в год` : ''}
{pinnedRaces.length>0 && (
{pinnedRaces.map(pinnedRace=>(
go('race-item', raceRouteParam(pinnedRace))}>
Главное событие
{pinnedRace.date.split(' ')[0]}{pinnedRace.date.split(' ').slice(1).join(' ')}

{pinnedRace.title}

{pinnedRace.place}
{!!pinnedRace.text &&

{excerptFromHtml(pinnedRace.text, 200)}

}
{pinnedRace.status}
))}
)}

Ближайшие старты

{!upcomingRaces.length &&
Ближайших стартов пока нет. Новые анонсы появятся здесь.
}
{regularRaces.map(r=>(
{r.date.split(' ')[0]}{r.date.split(' ').slice(1).join(' ')}
{r.img && {r.title}}
{r.title}
{r.place}
{!!r.text &&

{excerptFromHtml(r.text, 160)}

}
{r.status}
))}
); } function App(){ const cachedUser = readCachedAuthUser(); const [route,setRoute]=uS(()=>parseRouteFromLocation()); const [lang,setLang]=uS(()=>readPreferredLang()); const [loggedIn,setLoggedIn]=uS(()=>!!cachedUser); const [authChecked,setAuthChecked]=uS(false); const [siteDataLoading,setSiteDataLoading]=uS(true); const [dataVersion,setDataVersion]=uS(0); const [enroll,setEnroll]=uS([]); const [latestRejectedPayment,setLatestRejectedPayment]=uS(null); const [user,setUser]=uS(()=>cachedUser || DEFAULT_USER); const [tw,setTw]=uS(()=>{ try{return JSON.parse(localStorage.getItem('krsu_tw'))||{}}catch(e){return {}} }); const heroVariant = tw.hero || 'video'; uE(()=>{ fetch('api/bootstrap.php',{credentials:'same-origin'}) .then(res=>res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))) .then(payload=>{ if(payload && payload.ok && payload.data){ Object.assign(DATA, payload.data); setDataVersion(v=>v+1); } }) .catch(()=>{}) .finally(()=>setSiteDataLoading(false)); },[]); uE(()=>{ if(lang==='ky'){ applyGoogleTranslate('ky').catch(()=>{}); } },[lang]); uE(()=>{ const syncRoute = ()=>setRoute(parseRouteFromLocation()); window.addEventListener('hashchange', syncRoute); window.addEventListener('popstate', syncRoute); return ()=>{ window.removeEventListener('hashchange', syncRoute); window.removeEventListener('popstate', syncRoute); }; },[]); uE(()=>{ let cancelled = false; const fallbackUser = readCachedAuthUser(); function applyGuest(){ writeCachedAuthUser(null); setLoggedIn(false); setUser(DEFAULT_USER); setLatestRejectedPayment(null); } function applyUser(nextUser){ writeCachedAuthUser(nextUser); setUser(nextUser); setLoggedIn(true); setLatestRejectedPayment(null); if(!isStaffRole(nextUser.role) && !isLeagueMemberUser(nextUser)) loadEnroll(nextUser.email); } function checkAuth(attempt=0){ fetch('api/auth.php',{credentials:'same-origin'}) .then(res=>res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))) .then(payload=>{ if(cancelled) return; if(payload && payload.ok && payload.user){ applyUser(payload.user); setAuthChecked(true); return; } if(attempt===0 && fallbackUser){ setTimeout(()=>{ if(!cancelled) checkAuth(1); }, 350); return; } applyGuest(); setAuthChecked(true); }) .catch(()=>{ if(cancelled) return; if(attempt===0){ setTimeout(()=>{ if(!cancelled) checkAuth(1); }, 350); return; } if(fallbackUser){ setUser(fallbackUser); setLoggedIn(true); }else{ applyGuest(); } setAuthChecked(true); }); } checkAuth(); return ()=>{ cancelled = true; }; },[]); function loadEnroll(email=user.email){ if(!email) return; return fetch(`api/my-courses.php?email=${encodeURIComponent(email)}`,{credentials:'same-origin'}) .then(res=>res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))) .then(payload=>{ if(payload && payload.ok){ if(payload.user) setUser(prev=>({...prev, ...payload.user, role:payload.user.role || prev.role || 'user', cabinet_access:typeof payload.user.cabinet_access === 'boolean' ? payload.user.cabinet_access : !!prev.cabinet_access})); setLatestRejectedPayment(payload.latestRejectedPayment || null); return fetch('api/courses.php',{credentials:'same-origin'}) .then(res=>res.ok?res.json():Promise.reject(new Error(`HTTP ${res.status}`))) .then(coursePayload=>{ if(coursePayload?.ok&&Array.isArray(coursePayload.courses)) window.DATA.courses=coursePayload.courses; setEnroll(payload.enroll || []); }); } }) .catch(()=>{}); } uE(()=>{ window.scrollTo(0,0); },[route]); uE(()=>{ const baseTitle = 'КРСУ · Лаборатория БПЛА'; const newsList = window.DATA?.news || []; const courseItem = route.n==='course' ? (window.DATA?.courses || []).find(item=>item.id===route.p) : null; const newsArticle = route.n==='news-item' ? findNewsByParam(route.p, newsList) : null; const raceList = window.DATA?.races || []; const raceArticle = route.n==='race-item' ? findRaceByParam(route.p, raceList) : null; const staticPage = route.n==='page' ? (window.DATA?.pages || {})[String(route.p || '')] : null; const serviceItem = route.n==='service-detail' ? (window.DATA?.services || []).find(item=>item.slug===route.p) : null; const pageTitle = newsArticle ? `${newsArticle.title} — ${baseTitle}` : raceArticle ? `${raceArticle.title} — ${baseTitle}` : staticPage ? `${staticPage.title} — ${baseTitle}` : serviceItem ? (serviceItem.seo_title || `${serviceItem.name} — ${baseTitle}`) : route.n==='news' ? `Новости — ${baseTitle}` : route.n==='races' ? `Гонки дронов — ${baseTitle}` : route.n==='league' ? `Членство в Лиге — ${baseTitle}` : route.n==='pilot-rating' ? `Рейтинг пилотов — ${baseTitle}` : route.n==='education' ? `Каталог курсов — ${baseTitle}` : route.n==='services' ? `Услуги — ${baseTitle}` : route.n==='trial-lesson' ? `Пробный урок по аэронавигации — ${baseTitle}` : route.n==='autumn-school' ? `Осенняя школа дронов 2026 — ${baseTitle}` : route.n==='course' ? `${courseItem?.title || 'Курс'} — ${baseTitle}` : route.n==='faq' ? `FAQ — ${baseTitle}` : `${baseTitle} — обучение полётам на дронах`; const description = newsArticle ? excerptFromHtml(newsArticle.text, 180) : raceArticle ? excerptFromHtml(raceArticle.text || '', 180) || `Старт ${raceArticle.title} — ${raceArticle.place}.` : staticPage ? excerptFromHtml(staticPage.text || '', 180) : serviceItem ? (serviceItem.seo_description || excerptFromHtml(serviceItem.description || '', 180)) : route.n==='news' ? 'Новости КРСУ, партнёров и лаборатории БПЛА в одной ленте.' : route.n==='races' ? 'Анонсы соревнований, регистрации и старты дрон-рейсинга Лаборатории БПЛА.' : route.n==='league' ? 'Членство в Лиге Дронов: годовая подписка для участия в официальных заездах.' : route.n==='pilot-rating' ? 'Рейтинг пилотов Лаборатории БПЛА, очки, победы и лучшие круги сезона.' : route.n==='education' ? 'Каталог курсов Лаборатории БПЛА: пилотирование, инженерия, гонки и аэросъёмка.' : route.n==='services' ? 'Цифровые услуги Лаборатории БПЛА КРСУ для строительства, агросектора, инфраструктуры, обучения и продакшена.' : route.n==='trial-lesson' ? 'Бесплатный первый урок курса «Оператор БПЛА»: как беспилотник ориентируется в пространстве.' : route.n==='autumn-school' ? 'Осенняя школа дронов КРСУ для подростков 12–17 лет: онлайн-теория и лётная практика на Иссык-Куле.' : route.n==='course' ? (courseItem?.short || 'Курс Лаборатории БПЛА КРСУ.') : route.n==='faq' ? 'Часто задаваемые вопросы о программе подготовки операторов БПЛА в КРСУ.' : 'Лаборатория БПЛА КРСУ: обучение, новости, гонки, личный кабинет и курсы по беспилотникам.'; const image = new URL(newsArticle?.img || raceArticle?.img || serviceItem?.main_image || courseItem?.img || (route.n==='autumn-school' ? 'assets/autumn-school/autumn-school-01.jpg' : 'assets/logo-full.png'), document.baseURI).href; const url = location.href; document.title = pageTitle; updateMetaTag('name', 'description', description); updateMetaTag('property', 'og:title', pageTitle); updateMetaTag('property', 'og:description', description); updateMetaTag('property', 'og:type', newsArticle || raceArticle ? 'article' : route.n==='course' ? 'product' : 'website'); updateMetaTag('property', 'og:url', url); updateMetaTag('property', 'og:image', image); updateMetaTag('name', 'twitter:card', newsArticle?.img || raceArticle?.img || route.n==='autumn-school' ? 'summary_large_image' : 'summary'); updateMetaTag('name', 'twitter:title', pageTitle); updateMetaTag('name', 'twitter:description', description); updateMetaTag('name', 'twitter:image', image); },[route, dataVersion]); uE(()=>{ if(loggedIn && user.email && !isStaffRole(user.role) && !isLeagueMemberUser(user)) loadEnroll(user.email); },[dataVersion, loggedIn, route.n, user.email, user.role, user.league_member, user.leagueMember]); uE(()=>{ const waitingRoutes = ['cabinet','cab-materials','cab-certs','cab-settings','player','test']; if(!authChecked || !loggedIn || isStaffRole(user.role) || hasCabinetAccess(user) || !waitingRoutes.includes(route.n) || !user.email){ return; } let cancelled = false; async function refreshPendingAccess(){ try{ const [authRes] = await Promise.all([ fetch('api/auth.php',{credentials:'same-origin'}), loadEnroll(user.email), ]); const authPayload = await authRes.json().catch(()=>null); if(cancelled) return; if(authRes.ok && authPayload?.ok && authPayload.user){ setUser(prev=>({ ...prev, ...authPayload.user, role:authPayload.user.role || prev.role || 'user', cabinet_access:typeof authPayload.user.cabinet_access === 'boolean' ? authPayload.user.cabinet_access : !!prev.cabinet_access, })); } }catch(err){} } const timer = setInterval(refreshPendingAccess, 30000); return ()=>{ cancelled = true; clearInterval(timer); }; },[authChecked, loggedIn, route.n, user.email, user.role, user.cabinet_access]); uE(()=>{ const onTw=(e)=>{ if(e.data&&e.data.type==='tweaks-update'){ setTw(e.data.values||{}); localStorage.setItem('krsu_tw',JSON.stringify(e.data.values||{})); } }; window.addEventListener('message',onTw); return ()=>window.removeEventListener('message',onTw); },[]); uE(()=>{ const r=document.documentElement; if(tw.accent==='green') r.style.setProperty('--brand-cta','var(--green)'); document.body.style.setProperty('--radius', (tw.radius||16)+'px'); },[tw]); function go(n,p=null){ const nextRoute = {n,p}; const nextUrl = buildUrlFromRoute(n,p); const currentUrl = `${location.pathname}${location.hash}`; const targetUrl = nextUrl.replace(location.origin, ''); if(currentUrl === targetUrl){ setRoute(nextRoute); return; } history.pushState({}, '', nextUrl); setRoute(nextRoute); } function changeLang(nextLang){ if(nextLang==='ky'){ writePreferredLang('ky'); setLang('ky'); applyGoogleTranslate('ky').catch(()=>{}); return; } writePreferredLang('ru'); setLang('ru'); if(document.cookie.includes('googtrans=')){ clearGoogleTranslateCookie(); location.reload(); } } function onPaid(cid){ const now = new Date().toLocaleString('ru-RU', { timeZone:'Asia/Bishkek', day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit', }); setEnroll(e=> e.find(x=>x.id===cid) ? e.map(x=>x.id===cid?{...x,access:'pending',status:'Ожидает оплаты',pendingDate:now}:x) : [...e,{id:cid,access:'pending',progress:0,status:'Ожидает оплаты',testScore:null,pendingDate:now}]); setLatestRejectedPayment(prev=>prev?.courseId===cid ? null : prev); setTimeout(()=>loadEnroll(),300); } function onResult(cid,score){ setEnroll(e=>e.map(x=>x.id===cid?{...x,testScore:score,progress:100,status:'Завершён'}:x)); loadEnroll(); } function onProgress(cid,lessonIndex,totalLessons,completed=true){ setEnroll(items=>items.map(item=>{ if(item.id!==cid)return item; const legacyCount=Array.isArray(item.completedLessons)?0:Math.max(0,Math.min(totalLessons,Math.round(((item.progress||0)/100)*totalLessons))); const lessons=Array.isArray(item.completedLessons)?[...item.completedLessons]:Array.from({length:legacyCount},(_,index)=>index); const next=completed ? [...new Set([...lessons,lessonIndex])] : lessons.filter(index=>index!==lessonIndex); next.sort((a,b)=>a-b); const progress=Math.round(next.length/totalLessons*100); return {...item,completedLessons:next,progress,status:item.testScore!=null?item.status:(progress===100?'Материалы пройдены':progress>0?'В процессе':'Открыт')}; })); return fetch('api/my-courses.php',{ method:'PATCH', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify({email:user.email,course_id:cid,lesson_index:lessonIndex,total_lessons:totalLessons,completed}), }).then(async response=>{ const data=await response.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!response.ok||data.ok===false)throw new Error(data.error||`Не удалось сохранить прогресс (HTTP ${response.status})`); return data; }).then(data=>{ if(Array.isArray(data.completedLessons)){ setEnroll(items=>items.map(item=>item.id===cid?{...item,completedLessons:data.completedLessons,progress:Math.round(data.completedLessons.length/totalLessons*100)}:item)); } return data; }).catch(error=>{loadEnroll();throw error;}); } function onLogin(nextUser){ writeCachedAuthUser(nextUser); setUser(nextUser); setLoggedIn(true); setLatestRejectedPayment(null); if(!isStaffRole(nextUser.role) && !isLeagueMemberUser(nextUser)) loadEnroll(nextUser.email); if(route.n === 'admin' && isStaffRole(nextUser.role)){ go('admin', route.p); return; } if(isStaffRole(nextUser.role)){ go('admin'); return; } if(isLeagueMemberUser(nextUser)){ const leagueRoute = ['league-rating','league-infrastructure','league-discounts','league-pilot-card','league-support','cab-settings'].includes(route.n); go(leagueRoute ? route.n : 'league-rating', leagueRoute ? route.p : null); return; } if(isStudentAreaRoute(route.n)){ const studentRoute = ['cabinet','cab-materials','cab-certs','cab-settings','player','test','payment'].includes(route.n); go(studentRoute ? route.n : 'cabinet', studentRoute ? route.p : null); return; } go('cabinet'); } function logout(){ fetch('api/auth.php',{ method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'logout'}), }).catch(()=>{}); writeCachedAuthUser(null); setLoggedIn(false); setUser(DEFAULT_USER); setEnroll([]); setLatestRejectedPayment(null); go('home'); } function protectedView(content){ if(!authChecked){ return
Проверяю сессию…
; } return loggedIn ? content : ; } function publicView(content){ return publicLoading ? content :
{content}
; } const n=route.n; const fullscreen = ['login','cabinet','cab-materials','cab-certs','cab-settings','player','test','admin', 'league-rating','league-infrastructure','league-discounts','league-pilot-card','league-support'].includes(n); const publicLoading = siteDataLoading && ['home','services','service-detail','education','trial-lesson','autumn-school','course','news','news-item','media','races','race-item','league','pilot-rating','page','contacts','faq'].includes(n); const articleLoading = publicLoading && ['news-item','race-item','page'].includes(n); let view; if(n==='home') view=publicLoading ? : publicView(); else if(n==='services') view=publicLoading ? : publicView(); else if(n==='service-detail') view=publicLoading ? : publicView(); else if(n==='education') view=publicLoading ? : publicView(); else if(n==='trial-lesson') view=publicLoading ? : publicView(); else if(n==='autumn-school') view=publicView(); else if(n==='course') view=publicLoading ? : publicView(); else if(n==='news') view=publicLoading ? : publicView(); else if(n==='news-item') view=articleLoading ? : publicView(); else if(n==='media') view=publicLoading ? : publicView(); else if(n==='races') view=publicLoading ? : publicView(); else if(n==='race-item') view=articleLoading ? : publicView(); else if(n==='league') view=publicLoading ? : publicView(); else if(n==='pilot-rating') view=publicLoading ? : publicView(); else if(n==='public-pilot') view=publicView(); else if(n==='page') view=articleLoading ? : publicView(); else if(n==='contacts') view=publicLoading ? : publicView(); else if(n==='faq') view=publicLoading ? : publicView(); else if(n==='payment'){ if(!authChecked) view=protectedView(null); else if(!loggedIn) view=; else view=; } else if(n==='login') { const loginParts = String(route.p || '').split('/').filter(Boolean); const loginMode = loginParts[0]==='reg' ? 'reg' : loginParts[0]==='forgot' ? 'forgot' : loginParts[0]==='reset' ? 'reset' : 'login'; const resetToken = loginMode==='reset' ? decodeURIComponent(loginParts.slice(1).join('/')) : ''; view=; } else if(n==='admin') view=protectedView(isStaffRole(user.role) ? : ); else if(n==='player'){ if(!authChecked) view=protectedView(null); else if(!loggedIn) view=; else if(isLeagueMemberUser(user)) view=; else view=hasCabinetAccess(user) ? : ; } else if(n==='test'){ if(!authChecked) view=protectedView(null); else if(!loggedIn) view=; else if(isLeagueMemberUser(user)) view=; else view=hasCabinetAccess(user) ? : ; } else if(['cabinet','cab-materials','cab-certs','cab-settings', 'league-rating','league-infrastructure','league-discounts','league-pilot-card','league-support'].includes(n)){ if(!authChecked) view=protectedView(null); else if(!loggedIn) view=; else if(!hasCabinetAccess(user)) view=; else { let inner; const leagueMember = isLeagueMemberUser(user); const studentOnlyRoute = ['cabinet','cab-materials','cab-certs'].includes(n); const leagueRoute = ['league-rating','league-infrastructure','league-discounts','league-pilot-card','league-support'].includes(n); let activeRoute = n; if(leagueMember && studentOnlyRoute){ inner=; activeRoute='league-rating'; }else if(!leagueMember && leagueRoute){ inner=; activeRoute='cabinet'; }else if(n==='cabinet') inner=; else if(n==='cab-materials') inner=; else if(n==='cab-certs') inner=; else if(n==='cab-settings') inner={writeCachedAuthUser(nextUser);setUser(nextUser);}}/>; else inner=; view={inner}; } } else view=; return ( <> {!fullscreen &&
}
{view}
{!fullscreen &&