/* ===== Payment + Cabinet ===== */ const D3 = window.DATA; /* ---- fake QR ---- */ function QR({ size=180 }){ const n=21; const cells=[]; function finder(r,c){ return (r<7&&c<7)||(r<7&&c>=n-7)||(r>=n-7&&c<7); } let seed=7; const rnd=()=>{ seed=(seed*1103515245+12345)&0x7fffffff; return (seed>>8)&1; }; for(let r=0;r=2&&lr<=4&&lc>=2&&lc<=4)); } else { on=rnd()===1; } if(on) cells.push(); } return {cells}; } function Payment({ id, go, onPaid, user }){ const paymentId = String(id || ''); const [courseId] = paymentId.split('/'); // `racing` is kept for old links; new membership links use a dedicated // route id so they can never be confused with a catalog course. const isLeague = courseId === 'league-membership' || courseId === 'racing'; const c = D3.courses.find(x=>x.id===courseId) || D3.courses.find(x=>x.id===id) || D3.courses[0]; const league = D3.leagueMembership || {}; const productId = isLeague ? 'racing' : c.id; const contacts = D3.contacts || {}; // Education payments go directly to the KRSU bank account. Racing and // League payments are a separate flow and use their own QR code. const paymentMethod = isLeague ? 'qr' : 'bank'; const [agree,setAgree]=useState(false); const [payerName,setPayerName]=useState(user?.name || ''); const [payerEmail,setPayerEmail]=useState(user?.email || ''); const [sending,setSending]=useState(false); const [payError,setPayError]=useState(''); const selectedPrice = isLeague ? Number(league.yearlyPrice || 8760) : Number(c.price || 0); const paymentLabel = isLeague ? `${league.title || 'Членство в Лиге'} · Годовая подписка` : c.title; useEffect(()=>{ setPayerName(user?.name || ''); setPayerEmail(user?.email || ''); },[user?.name, user?.email]); async function submitPayment(){ if(!agree || sending) return; setSending(true); setPayError(''); try{ const res = await fetch('api/payments.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ course_id:productId, name:payerName, email:payerEmail, amount:selectedPrice, payment_method:paymentMethod, comment:isLeague ? paymentLabel : 'Заявка отправлена с сайта', }), }); const data = await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok || data.ok===false) throw new Error(data.error || `HTTP ${res.status}`); onPaid && onPaid(productId); go('cabinet'); }catch(err){ setPayError(err.message || 'Не удалось отправить заявку'); }finally{ setSending(false); } } return (
Оплата

{isLeague ? 'Оплата гонок по QR-коду' : 'Оплата обучения по реквизитам КРСУ'}

{isLeague && (
Лига дронов {league.title || 'Членство в Лиге'} {league.subtitle || 'Платная регистрация для участия в Лиге Дронов и официальных заездах.'} Годовая подписка
{D3.fmt(selectedPrice)} сом в год
)} {isLeague ? (
{contacts.qrImage ? QR-код для оплаты гонок : }
Отдельный счёт гонок · тестовый QR Отсканируйте QR-код для оплаты

{contacts.paymentText || 'Откройте приложение банка, наведите камеру на QR-код гонок и подтвердите перевод.'}

К оплате{D3.fmt(selectedPrice)} сом
) : (
Банковские реквизиты

{contacts.bankPaymentText || 'Оплата обучения принимается только по банковским реквизитам КРСУ. В назначении платежа укажите выбранный курс.'}

Получатель{contacts.receiver || 'КРСУ · Лаборатория БПЛА'}
Расчётный счёт{contacts.account}
БИК{contacts.bik}
ИНН{contacts.inn}
{contacts.okpo &&
ОКПО{contacts.okpo}
}
Назначение{`Оплата курса «${c.title}»`}
)}
{isLeague ? 'После оплаты администратор подтвердит членство в Лиге — обычно в течение рабочего дня.' : 'После оплаты администратор вручную откроет доступ к курсу в вашем личном кабинете — обычно в течение рабочего дня.'}
); } /* ---------------- LOGIN ---------------- */ const PERSON_NAME_PATTERN=/^[\p{L}\p{M}]+(?:['’.-][\p{L}\p{M}]+)*(?:\s+[\p{L}\p{M}]+(?:['’.-][\p{L}\p{M}]+)*){1,5}$/u; function Login({ go, onLogin, initialMode='login', resetToken='' }){ const resolveMode = value => ['reg','forgot','reset'].includes(value) ? value : 'login'; const [mode,setMode]=useState(resolveMode(initialMode)); const [sending,setSending]=useState(false); const [authError,setAuthError]=useState(''); const [authMessage,setAuthMessage]=useState(''); const [regValues,setRegValues]=useState({name:'',email:'',password:''}); const [regTouched,setRegTouched]=useState({name:false,email:false,password:false}); const normalizedRegName=regValues.name.trim().replace(/\s+/g,' '); const normalizedRegEmail=regValues.email.trim(); const emailLocalPart=normalizedRegEmail.split('@')[0] || ''; const regErrors={ name:normalizedRegName.length<5 ?'Введите имя и фамилию — минимум 5 символов.' :normalizedRegName.length>80 ?'Имя и фамилия должны быть не длиннее 80 символов.' :PERSON_NAME_PATTERN.test(normalizedRegName) ?'' :'Введите имя и фамилию: 2–6 слов, только буквы, дефис или апостроф.', email:normalizedRegEmail.length>190 ?'Email должен быть не длиннее 190 символов.' :emailLocalPart.length>64 ?'Часть email до знака @ должна быть не длиннее 64 символов.' :/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedRegEmail) ?'' :'Введите корректный email, например name@mail.kg.', password:regValues.password.length<10?'Пароль должен содержать не менее 10 символов.':'', }; const regValid=!regErrors.name&&!regErrors.email&&!regErrors.password; useEffect(()=>{ setMode(resolveMode(initialMode)); setAuthError(''); setAuthMessage(''); setRegTouched({name:false,email:false,password:false}); },[initialMode, resetToken]); function switchMode(nextMode){ setMode(nextMode); setAuthError(''); setAuthMessage(''); setRegTouched({name:false,email:false,password:false}); } function updateRegistration(field,value){ const limits={name:80,email:190}; const nextValue=limits[field] ? value.slice(0,limits[field]) : value; setRegValues(current=>({...current,[field]:nextValue})); setAuthError(''); } async function submit(e){ e.preventDefault(); const fields=e.currentTarget.elements; if(mode==='reg'&&!regValid){ setRegTouched({name:true,email:true,password:true}); setAuthError('Проверьте выделенные поля регистрации.'); return; } setSending(true); setAuthError(''); setAuthMessage(''); try{ const payload = mode==='forgot' ? {action:'request_password_reset', email:fields.email.value} : mode==='reset' ? {action:'reset_password', token:resetToken, password:fields.password.value} : { action:mode==='reg'?'register':'login', name:fields.name?.value || '', email:fields.email.value, password:fields.password.value, }; if(mode==='reset' && fields.password.value !== fields.repeat_password.value){ throw new Error('Пароли не совпадают'); } const res = await fetch('api/auth.php',{ method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload), }); const data = await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok || data.ok===false) throw new Error(data.error || `HTTP ${res.status}`); if(mode==='login' || mode==='reg'){ onLogin(data.user); }else if(mode==='forgot'){ setAuthMessage(data.message || 'Если email найден, письмо уже отправлено.'); }else if(mode==='reset'){ setAuthMessage(data.message || 'Пароль обновлён.'); setMode('login'); } }catch(err){ setAuthError(err.message || 'Не удалось войти'); }finally{ setSending(false); } } return (
Студенты лаборатории БПЛА

Ваш путь в небо начинается с личного кабинета

Доступ к курсам, материалам, симулятору и аттестации — в одном месте.

{mode==='login' ? 'Вход в личный кабинет' : mode==='reg' ? 'Регистрация' : mode==='forgot' ? 'Восстановление пароля' : 'Новый пароль'}

{mode==='login' ? 'Введите данные для входа' : mode==='reg' ? 'Создайте аккаунт, чтобы начать обучение' : mode==='forgot' ? 'Введите email, и мы отправим ссылку для смены пароля' : 'Введите новый пароль для личного кабинета'}

{mode==='reg' &&
updateRegistration('name',e.target.value)} onBlur={()=>setRegTouched(v=>({...v,name:true}))} placeholder="Айбек Усенов" minLength={5} maxLength={80} autoComplete="name" aria-invalid={regTouched.name&&!!regErrors.name} aria-describedby="reg-name-hint" required/>
{regTouched.name?(regErrors.name||'Имя и фамилия заполнены правильно.'):'2–6 слов: буквы, дефис или апостроф.'}{regValues.name.length}/80
} {mode!=='reset' &&
updateRegistration('email',e.target.value):undefined} onBlur={mode==='reg'?()=>setRegTouched(v=>({...v,email:true})):undefined} placeholder="you@mail.kg" maxLength={190} autoComplete="email" aria-invalid={mode==='reg'&®Touched.email&&!!regErrors.email} aria-describedby={mode==='reg'?'reg-email-hint':undefined} required/> {mode==='reg'&&
{regTouched.email?(regErrors.email||'Email заполнен правильно.'):'Введите email в формате name@mail.kg.'}{regValues.email.length}/190
}
} {mode!=='forgot' &&
updateRegistration('password',e.target.value):undefined} onBlur={mode==='reg'?()=>setRegTouched(v=>({...v,password:true})):undefined} placeholder="••••••••••" minLength={10} autoComplete={mode==='login'?'current-password':'new-password'} aria-invalid={mode==='reg'&®Touched.password&&!!regErrors.password} aria-describedby={mode==='reg'?'reg-password-hint':undefined} required/>{mode==='reg'&&
=10?'is-success':''}`}>=10?'checkCircle':'lock'} size={14}/> Не менее 10 символов{regValues.password.length}/10
}
} {mode==='reset' &&
} {authError &&
{authError}
} {authMessage &&
{authMessage}
}
{mode==='login' && <>Нет аккаунта? } {mode==='reg' && <>Уже есть аккаунт? } {mode==='forgot' && <>Вспомнили пароль? } {mode==='reset' && <>Нужна обычная форма входа? }
{mode==='login' &&
}
); } Object.assign(window, { Payment, Login, QR });