Logo GH

Окуя-бот жана чат операциялары

1) Максаты жана баалуулугу

Инцидент-бот - бул түздөн-түз корпоративдик чаттан (Slack/Teams/Telegram) инцидентти башкаруу интерфейси: ондогон системалардагы текстти → аракеттерди бир киргизүү. Ал:
  • MTTA/MTTR автоматташтыруу менен кыскартат;
  • фактылардын (SoT) жана коммуникациялардын бирдиктүү контурун түзөт;
  • далилдүүлүктү камсыз кылат (аудит, таймлайндар, жаңылануулар SLA);
  • консолдордо on-call жана "көмүү" боюнча жүктү азайтат.

2) Чат операцияларында ролдору жана RACI

Incident Commander (IC) - окуя ээси: ачуу/жабуу, артыкчылык, чечимдер.
Comms Lead (CL) - тексттер жана жаңыртуу тартиби (тышкы/ички).
Domain Leads (Payments/Games/Core/Infra) - техникалык жана фикстер.
Scribe - таймлайн, иш-аракеттер журналы.
Bot Admin - укуктар/саясат/ботту бириктирүү.

Эреже: бир окуя бир IC жана бир CL; ролун өзгөртүү - так бот командасы.

3) Базалык сценарийлер (end-to-end)

1. Окуя башталышы: alert → '/incident new p1 "Deposits EU down" '→ бот картаны түзөт, var-room, IC/CL дайындайт, биринчи жаңылыктын таймерин коёт.
2. Ведение: `/incident add-facts`, `/incident status set degraded`, `/incident assign @payments-lead`, `/incident timer 20m`.
3. Байланыш: '/incident publish status '(CL долбоору), '/incident partners notify', '/incident regulator draft '.
4. Действия: `/runbook psp-failover PSP1→PSP2`, `/feature toggle replay-center off 60m`, `/traffic shift 30% eu→uk`.
5. Жабуу жана пост-мортем: '/incident resolve ', авто-чогултуу тайм-лайн, '/postmortem generate'.

4) Boot буйрук (негизги)

Түзүү/классификация

`/incident new p{1|2|3|4} "" [components=…] [regions=…]`</p> <p>`/incident severity set p2`, `/incident tag add payments,psp`</p> <p>Ээлик кылуу жана ролдору</p> <p>`/incident ic @user`, `/incident comms @user`, `/incident assign @user [domain]`</p> <p>Таймери жана SLO жаңыртуулар</p> <p><b>'/incident next-update 15m ', '/incident remind' (бот пингет CL), '/incident eta set 18:</b> 30 '</p> <p>Фактылар жана статус</p> <p>`/incident fact "auth-success PSP1 -25% TR/EU"`, `/incident status {investigating|degraded|monitoring|resolved}`</p> <p>Комм-пакеттер</p> <p>`/incident draft public|partners|regulator`, `/incident publish public`</p> <p>Интеграция</p> <p>'/runbook <action> '(auto-action каталогу), '/dashboard link <id>', '/ticket create <system> '</p> <p>Жабуу/пост-мортем</p> <p>`/incident resolve [reason=…]`, `/postmortem generate`, `/postmortem assign @owner`</p> <div style="display:flex; align-items:flex-start; gap:12px; padding:12px 14px; margin:16px 0; border-radius:12px; background:#0f1624; color:#e6edf3; border-left:4px solid #2f9aff; border:1px solid rgba(255,255,255,0.05); box-shadow:0 0 12px rgba(255,255,255,.03);"> <span style="font-size:18px; line-height:1.2; color:#5ab8ff;">💡</span> <span style="flex:1; opacity:.95;">Бардык командалар убакыт, жазуучу, параметрлери жана натыйжасы менен чечмеленет.</span> </div> <p>5) Интеграция (минималдуу зарыл)</p> <p>Мониторинг/Observability: Alerty, SLI/SLO (burn-rate), dashbord links.<br>Incident Manager (ITSM): статус/талааларды эки тараптуу синхрондоштуруу.<br>Статус-бет: CL (policy-gate) аркылуу долбоорлор жана жарыялоо.<br>Провайдерлер (PSP/KYC/Оюн студиялары): байланыш каталогдору, тез каттар/каналдар.<br>Release/Feature Flags: Канар бут/спины, релиздерге шилтемелер.<br>Runbooks/Auto-remediation: guardrails менен коопсуз иш каталогу.<br>CMDB/ээлери: домендик лиддердин авто максаты, эскалация.<br>Time Line сактоо: Аудит/Post-Мортем үчүн WORM/immutable.</p> <p>6) Боттун архитектурасы</p> <p>Gateway (Chat Adapter): Slack/Teams/телеграмма интерфейстери.<br>Command Parser + Policy Engine: авторизация, валидация, SoD жана чектер.<br>Orchestrator: окуя жагдайлар, убакыт, эскертүүлөр.<br>Integrations Layer: ITSM, мониторинг, статус-бет, релиздер, runbooks үчүн кардарлар.<br>Evidence Store: окуялар, фактылар, билдирүүлөр, тиркемелер (WORM).<br>Metrics & Audit: Сапат көрсөткүчтөрү, иш-аракеттердин Логи, командаларды издөө.</p> <p>7) Саясат, укуктар жана коопсуздук</p> <p>RBAC/ABAC: ким жаратат/жабат, сүйүүчүлүктү өзгөртөт, сыртка жарыялайт.<br>SoD: Comms publish CL ролун талап кылат; жогорку тобокелдик иш-аракет (PSP-роутинг, PII-экспорт) - эки контролдоо.<br>JIT укугу: окуя учурунда домендик лидерлерге убактылуу берүү.<br>Кол коюу жана шифрлөө: HMAC/mTLS.<br>"Fat-finger" коргоо: коркунучтуу команда ырастоо, dry-run жана иш-аракет үчүн TTL.<br>PII-гигиена: долбоорлор/блогдордо жашыруу; ачык каналдарда PII тыюу салуу.</p> <p>8) автоматташтыруу агымдары (мисал)</p> <p>Alert P1 → бот вар-рум ('#inc -2025-11-01-001') түзөт, нөөмөттө (IC, CL, Payments/Infra).<br>Дашбордддорду/SLI байлап, ITSMге билет ачат, биринчи коомдук жаңылануунун үлгүсүн даярдайт.<br>Таймерлерди коёт: "кийинки жаңыртуу 15 мүнөттөн кийин", CL эскертүүлөр.<br>Предлагает runbooks: “PSP reroute 30% → PSP2”, “degrade replay-center”, “autoscale settle-workers”.<br>Жарыяланган учурда - тексттин версиясын жазып алып, статустук/социалдык тармактарга (CL аркылуу) жарыялайт.<br>Жабылганда - таймлайн, метриканы, пост-мортеманын долбоорун, VIP/өнөктөштөргө жөнөтүүнү чогултат.</p> <p>9) Таймлайндар жана далилдүүлүк</p> <p>Ар бир окуя: 'T + мм: баяндамасы, жазуучу/бот, команда, натыйжасы, шилтемелер'.<br>Билдирүүлөрдүн редакциялары (diff), релиздерге/фичфлагтарга/пландуу иштерге байлоо колдоого алынат.<br>Экспорт: аудит жана жөнгө салуу үчүн PDF/CSV.</p> <p>10) Метрика (KPI/KRI ChatOps)</p> <p>MTTA (чат): Алертадан '/incident new '.<br>MTTS (setup): war-room даяр жана ролдорду дайындоо чейин.<br>Cadence adherence: коомдук тактоо интервалдарын сактоо.<br>Runbook usage rate: автоматташтырылган иш-аракеттер менен окуялар үлүшү.<br>Consistency score: каналдардын ортосундагы айырмачылыктар = 0 - максат.<br>Pager fatigue ↓: Ошол эле/мыкты SLO менен кол чалуу азайтуу.<br>Postmortem SLA: D5 ≤ чогултулган пост-мортемалардын үлүшү.</p> <p>11) Шаблон каталогу (үзүндүлөр)</p> <u style="text-underline-offset:7px; text-decoration-color:rgba(200, 220, 255, 0.35);">P1 түзүү:</u> <pre style="margin:14px 0; padding:12px 14px; background:#0f1624; color:#e5e7eb; border:1px solid rgba(255,255,255,.10); border-radius:12px; overflow:auto; box-shadow:0 0 12px rgba(255,255,255,.03); direction:ltr; text-align:left; unicode-bidi:bidi-override;"><code> /incident new p1 "Deposits EU down" components=payments,deposits regions=EU</code></pre> <u style="text-underline-offset:7px; text-decoration-color:rgba(200, 220, 255, 0.35);">Биринчи коомдук жаңылануу (CL аркылуу):</u> <pre style="margin:14px 0; padding:12px 14px; background:#0f1624; color:#e5e7eb; border:1px solid rgba(255,255,255,.10); border-radius:12px; overflow:auto; box-shadow:0 0 12px rgba(255,255,255,.03); direction:ltr; text-align:left; unicode-bidi:bidi-override;"><code> /incident draft public /incident publish public</code></pre> <u style="text-underline-offset:7px; text-decoration-color:rgba(200, 220, 255, 0.35);">PSP роутинг жана phich деградация:</u> <pre style="margin:14px 0; padding:12px 14px; background:#0f1624; color:#e5e7eb; border:1px solid rgba(255,255,255,.10); border-radius:12px; overflow:auto; box-shadow:0 0 12px rgba(255,255,255,.03); direction:ltr; text-align:left; unicode-bidi:bidi-override;"><code> /runbook psp-failover PSP1→PSP2 30% /feature toggle replay-center off 45m</code></pre> <u style="text-underline-offset:7px; text-decoration-color:rgba(200, 220, 255, 0.35);">Пост-мортем:</u> <pre style="margin:14px 0; padding:12px 14px; background:#0f1624; color:#e5e7eb; border:1px solid rgba(255,255,255,.10); border-radius:12px; overflow:auto; box-shadow:0 0 12px rgba(255,255,255,.03); direction:ltr; text-align:left; unicode-bidi:bidi-override;"><code> /postmortem generate /postmortem assign @owner</code></pre> <p>12) процесстерге киргизүү</p> <p>Коммуникация: "Инциденттердеги коммуникация" жана "Системанын статусу барактары" менен байланыш.<br>Байкоо: SLO/SLI жана синтетика боюнча тез шилтемелер; графиктердин автотиркемеси.<br>Alerting: P1/P2 учурунда автосоздоо окуя; дедуп сигналдар бир агым.<br>Auto-түзөтүүлөр: guardrails жана rockers менен бир туташтыргыч runbooks.<br>Workflow Engine: human-tasks (4-eyes), эскалация таймерлери, чек баракчалары.</p> <p>13) Жол картасы киргизүү (4-8 жума)</p> <p>Нед. 1-2: MVP команда: '/incident new ', ролу (IC/CL), war-room, update таймер, ITSM жана мониторинг менен байланыш.<br>Нед. 3-4: билдирүү шаблондору (ачык/өнөктөштөр/жөнгө салуучулар), статус-бет (долбоор → жарыялоо), 5-7 runbooks каталогу.<br>Нед. 5-6: policy-as-code (RBAC/SoD/JIT), жогорку тобокелдик боюнча кош көзөмөл, WORM журналы, дашборд KPI ChatOps.<br>Нед. 7-8: tabletop-машыгуу P1/P2, релиздер/fichflags менен бириктирүү, auto-чогултуу post-мортема, локалдаштыруу.</p> <p>14) Антипаттерндер</p> <p>"Бардык бот аркылуу" жок guardrails → кокусунан коркунучтуу иш-аракеттер.<br>CL/Legal-review ролу жок статустук бетине жарыялоо.<br>Логиндер/версиялары жок командалар → далилденбегендик.<br>Татаал формалар (20 + талаа) чатта - ылдамдык төмөндөйт; жакшыраак кыска буйрук + шилтемелер.<br>Жок таймери updates → P1 боюнча "унчукпай".<br>CMDB/ээлери менен интеграциянын жоктугу → дайындоолордо башаламандык.</p> <p>15) Жыйынтык</p> <p>Инцидент-бот жана ChatOps - бул "командалар менен бот" эмес, операциялык платформа: инциденттин тез башталышы, жаңылануу тартиби, коопсуз чектөөлөр менен автоматташтырылган аракеттер, байкоо жүргүзүү жана далилдөө. Мындай контур алдын ала MTTR азайтат, байланыштын сапатын жогорулатат жана эң жогорку учурларда iGaming бизнесинин кирешесин коргойт.</p> </article> </div> </section> <style> /* =================== INPUT SYSTEM =================== */ .gh-input-h { --h:44px; height:var(--h); } .gh-chip, .gh-ccode, .gh-text, .gh-tel{ height:var(--h); padding:.55rem .75rem; border-radius:1rem; font-size:.875rem; line-height:1.2; border:1px solid rgba(255,255,255,.15); background:rgba(16,26,46,.6); color:#fff; outline:none; transition:border-color .15s ease, box-shadow .15s ease, background-color .15s ease; } .gh-chip{ width:4.5rem; display:inline-flex; align-items:center; justify-content:center; color:#cbd5e1; } .gh-ccode{ width:4.5rem; text-align:center; } .gh-ccode[readonly]{ color:#cbd5e1; cursor:default; user-select:none; } .gh-text, .gh-tel{ flex:1; min-width:0; } .gh-group{ display:flex; gap:.5rem; align-items:stretch; } .gh-focus:focus{ box-shadow:0 0 0 2px rgba(47,154,255,.25); border-color:rgba(47,154,255,.55); } /* =================== LABEL ROW =================== */ .gh-label-row{ display:grid; grid-template-columns: auto max-content; align-items:center; gap:.5rem; margin-bottom:.375rem; } .gh-label-row .gh-label{ font-size:.75rem; color:#94a3b8; } .gh-label-row .gh-ghost{ justify-self:end; text-align:center; white-space:nowrap; font-size:.675rem; letter-spacing:.06em; text-transform:uppercase; border:1px dashed rgba(255,255,255,.18); color:#9aa4b2; border-radius:999px; padding:.15rem .45rem; opacity:.85; transition:opacity .2s ease, color .2s ease, border-color .2s ease; pointer-events:none; } .gh-label-row .gh-ghost.-stub{ opacity:0; border-color:transparent; } .gh-optional.is-active .gh-ghost{ opacity:.25; color:#94a3b8; border-color:rgba(255,255,255,.12); } .gh-error{ box-shadow:0 0 0 2px rgba(244,63,94,.45)!important; border-color:rgba(244,63,94,.6)!important; } .gh-field + .gh-field{ margin-top:.9rem; } .gh-hint{ margin-top:.35rem; font-size:.6875rem; color:#94a3b8; } @media (max-width: 360px){ .gh-chip, .gh-ccode{ width:4.1rem; } } /* ====== КНОПКА быстрых контактов ====== */ /* Базово — ведёт себя как обычная inline-flex кнопка */ .gh-qs-trigger { position: relative; /* z-index не задаём, чтобы не лезла выше оверлеев */ transition: opacity .2s ease, transform .2s ease, visibility .2s ease; } /* Когда открыт теговый список — кнопку полностью прячем */ html.tag-modal-open .gh-qs-trigger { pointer-events: none; opacity: 0; visibility: hidden; transform: translateY(4px); } </style> <!-- CONTACT --> <section id="contact" class="py-12 lg:py-16"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <!-- Заголовок --> <div class="mb-4"> <div class="-mx-4 dir-ltr"> <span class="ml-4 text-[10px] uppercase tracking-widest font-semibold text-aqua-300 bg-aqua-500/10 border border-aqua-500/30 rounded-full px-2 py-0.5 inline-block mb-2"> Contact </span> <div class="fade-mask"> <div class="fade-inner gh-scrollbar-none touch-x px-4"> <h2 class="text-3xl font-bold text-white drop-shadow-[0_0_8px_rgba(47,154,255,0.35)] whitespace-nowrap"> Биз менен байланышыңыз </h2> </div> </div> </div> </div> <div class="mt-8 grid md:grid-cols-2 gap-10 lg:gap-12 items-start"> <!-- Левая колонка --> <div class="space-y-3 text-sm"> <p class="text-slate-400 text-[13px] pl-0 sm:pl-[2.4rem]" style="line-height:1.25; margin-bottom:18px;"> <span style="display:block; font-weight:800; margin-bottom:11px;">Кандай гана суроо же колдоо керек болбосун — бизге кайрылыңыз.</span><span style="display:block; height:5px;"></span><span style="display:block; font-weight:400;">Биз дайым жардам берүүгө даярбыз!</span> </p> <!-- Email --> <div class="flex items-start gap-2"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414zM0 4.697v7.104l5.803-3.558zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586zm3.436-.586L16 11.801V4.697z"/> </svg> <div> <div class="font-medium text-white">Email</div> <a href="mailto:support@gamblehub.io?subject=Gamble%20Hub%20%D0%B1%D0%BE%D1%8E%D0%BD%D1%87%D0%B0%20%D1%81%D1%83%D1%80%D0%BE%D0%BE&body=%D0%A1%D0%B0%D0%BB%D0%B0%D0%BC%D0%B0%D1%82%D1%81%D1%8B%D0%B7%D0%B1%D1%8B%21%0A%0A%D0%A1%D0%B8%D0%B7%D0%B4%D0%B8%D0%BD%20%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D1%83%D2%A3%D1%83%D0%B7%2F%D0%BA%D1%8B%D0%B7%D0%BC%D0%B0%D1%82%D1%8B%D2%A3%D1%8B%D0%B7%20%D0%B1%D0%BE%D1%8E%D0%BD%D1%87%D0%B0%20%D1%81%D1%83%D1%80%D0%BE%D0%BE%20%D0%B1%D0%B5%D1%80%D0%B3%D0%B8%D0%BC%20%D0%BA%D0%B5%D0%BB%D0%B5%D1%82.%0A%0A%D3%A8%D0%B7%D2%AF%D2%A3%D2%AF%D0%B7%D0%B4%D3%A9%D1%80%D0%B3%D3%A9%20%D1%8B%D2%A3%D0%B3%D0%B0%D0%B9%D0%BB%D1%83%D1%83%20%D1%83%D0%B1%D0%B0%D0%BA%D1%82%D0%B0%20%D0%B1%D0%B0%D0%B9%D0%BB%D0%B0%D0%BD%D1%8B%D1%88%D1%8B%D2%A3%D1%8B%D0%B7%D0%B4%D0%B0%D1%80.%0A%0A%D0%A0%D0%B0%D1%85%D0%BC%D0%B0%D1%82%21" class="text-slate-400 text-xs break-all hover:text-aqua-400 transition dir-ltr" rel="nofollow noopener"> support@gamblehub.io </a> </div> </div> <!-- Telegram --> <div class="flex items-start gap-2"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.287 5.906q-1.168.486-4.666 2.01-.567.225-.595.442c-.03.243.275.339.69.47l.175.055c.408.133.958.288 1.243.294q.39.01.868-.32 3.269-2.206 3.374-2.23c.05-.012.12-.026.166.016s.042.12.037.141c-.03.129-1.227 1.241-1.846 1.817-.193.18-.33.307-.358.336a8 8 0 0 1-.188.186c-.38.366-.664.64.015 1.088.327.216.589.393.85.571.284.194.568.387.936.629q.14.092.27.187c.331.236.63.448.997.414.214-.02.435-.22.547-.82.265-1.417.786-4.486.906-5.751a1.4 1.4 0 0 0-.013-.315.34.34 0 0 0-.114-.217.53.53 0 0 0-.31-.093c-.3.005-.763.166-2.984 1.09"/> </svg> <div> <div class="font-medium text-white">Telegram</div> <a href="https://t.me/Gamble_GC" target="_blank" rel="nofollow noopener noreferrer" class="text-slate-400 text-xs hover:text-aqua-400 transition dir-ltr"> @Gamble_GC </a> </div> </div> <!-- WhatsApp --> <div class="flex items-start gap-2 mt-3"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M13.601 2.326A7.85 7.85 0 0 0 7.994 0C3.627 0 .068 3.558.064 7.926c0 1.399.366 2.76 1.057 3.965L0 16l4.204-1.102a7.9 7.9 0 0 0 3.79.965h.004c4.368 0 7.926-3.558 7.93-7.93A7.9 7.9 0 0 0 13.6 2.326zM7.994 14.521a6.6 6.6 0 0 1-3.356-.92l-.240-.144-2.494.654.666-2.433-.156-.251a6.56 6.56 0 0 1-1.007-3.505c0-3.626 2.957-6.584 6.591-6.584a6.56 6.56 0 0 1 4.66 1.931 6.56 6.56 0 0 1 1.928 4.66c-.004 3.639-2.961 6.592-6.592 6.592m3.615-4.934c-.197-.099-1.17-.578-1.353-.646-.182-.065-.315-.099-.445.099-.133.197-.513.646-.627.775-.114.133-.232.148-.43.05-.197-.1-.836-.308-1.592-.985-.59-.525-.985-1.175-1.103-1.372-.114-.198-.011-.304.088-.403.087-.088.197-.232.296-.346.10-.114.133-.198.198-.33.065-.134.034-.248-.015-.347-.05-.099-.445-1.076-.612-1.47-.16-.389-.323-.335-.445-.34-.114-.007-.247-.007-.38-.007a.73.73 0 0 0-.529.247c-.182.198-.691.677-.691 1.654s.71 1.916.81 2.049c.098.133 1.394 2.132 3.383 2.992.47.205.84.326 1.129.418.475.152.904.129 1.246.08.38-.058 1.171-.48 1.338-.943.164-.464.164-.86.114-.943-.049-.084-.182-.133-.38-.232"/> </svg> <div> <div class="font-medium text-white">WhatsApp</div> <a href="https://wa.me/447893931020" target="_blank" rel="nofollow noopener noreferrer" class="text-slate-400 text-xs hover:text-aqua-400 transition dir-ltr"> +44 7893 931 020 </a> </div> </div> <!-- Триггер быстрой формы --> <a href="#" class="gh-qs-trigger relative inline-flex items-center justify-center gap-2 px-5 pr-10 py-2 rounded-full text-[13px] font-semibold text-white border border-white/10 bg-gradient-to-r from-aqua-600/40 to-brand-500/40 hover:from-aqua-600/60 hover:to-brand-500/60 focus:outline-none focus:ring-2 focus:ring-aqua-400/50 transition-all duration-200" data-qs-open data-qs-subject="Quick: Биз менен байланышыңыз" data-qs-pref="email"> <span class="relative z-10"> Интеграцияны баштоо </span> <!-- SVG "палец" --> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" class="absolute right-3 top-1/2 -translate-y-1/2 w-5 h-5 opacity-95 pointer-events-none drop-shadow-[0_2px_8px_rgba(47,154,255,0.35)]"> <path d="M8.5 1.75v2.716l.047-.002c.312-.012.742-.016 1.051.046.28.056.543.18.738.288.273.152.456.385.56.642l.132-.012c.312-.024.794-.038 1.158.108.37.148.689.487.88.716q.113.137.195.248h.582a2 2 0 0 1 1.99 2.199l-.272 2.715a3.5 3.5 0 0 1-.444 1.389l-1.395 2.441A1.5 1.5 0 0 1 12.42 16H6.118a1.5 1.5 0 0 1-1.342-.83l-1.215-2.43L1.07 8.589a1.517 1.517 0 0 1 2.373-1.852L5 8.293V1.75a1.75 1.75 0 0 1 3.5 0"/> </svg> </a> </div> <!-- Правая колонка: форма --> <div class="relative rounded-3xl border border-white/15 bg-gradient-to-br from-night-800/80 to-night-900/80 p-6 backdrop-blur-sm"> <!-- Toast для основной формы --> <div id="ghToastMain" class="pointer-events-none absolute inset-x-4 top-[30rem] z-50 -translate-y-2 opacity-0 transition-all duration-300"></div> <form id="ghContactForm" class="space-y-4" autocomplete="off" novalidate> <!-- honeypot / служебные --> <input type="text" name="company" tabindex="-1" autocomplete="off" class="hidden" aria-hidden="true"/> <input type="hidden" name="started_at" id="ghStartedAt"/> <input type="hidden" name="channel" id="ghChannel" value="email"/> <input type="hidden" name="whatsapp_full" id="ghWaFull" value=""/> <p class="text-xs text-slate-400"> Email — <span class="text-slate-200">милдеттүү</span>. Telegram же WhatsApp — <span class="text-slate-300">каалооңузга жараша</span>. </p> <!-- Name --> <div class="gh-field" data-field="name"> <div class="gh-label-row"> <span class="gh-label">Атыңыз</span> <span class="gh-ghost -stub">милдеттүү эмес</span> </div> <input id="name" name="name" type="text" required class="gh-text gh-input-h gh-focus w-full" placeholder="Азат Беков"/> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Email --> <div class="gh-field" data-field="email" id="fieldEmail"> <div class="gh-label-row"> <span class="gh-label">Email</span> <span class="gh-ghost -stub">милдеттүү эмес</span> </div> <input id="email" name="email" type="email" required class="gh-text gh-input-h gh-focus w-full" placeholder="you@company.com"/> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Subject --> <div class="gh-field" data-field="subject"> <div class="gh-label-row"> <span class="gh-label">Тема</span> <span class="gh-ghost -stub">милдеттүү эмес</span> </div> <input id="subject" name="subject" type="text" required class="gh-text gh-input-h gh-focus w-full" placeholder="Интеграция, баа, өнөктөштүк боюнча суроо…"/> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Message --> <div class="gh-field" data-field="message"> <div class="gh-label-row"> <span class="gh-label">Билдирүү</span> <span class="gh-ghost -stub">милдеттүү эмес</span> </div> <textarea id="message" name="message" rows="4" required class="w-full rounded-2xl border border-white/15 bg-night-700/60 px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-aqua-500/40" placeholder="Кыскача түрдө эмне керек экенин сүрөттөп бериңиз"></textarea> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Telegram (optional) --> <div class="gh-field gh-optional" data-field="telegram" id="fieldTelegram"> <div class="gh-label-row"> <span class="gh-label">Telegram</span> <span class="gh-ghost">милдеттүү эмес</span> </div> <div class="gh-group"> <span class="gh-chip gh-input-h">@</span> <input id="telegram" name="telegram" type="text" inputmode="text" autocomplete="off" maxlength="32" class="gh-text gh-input-h gh-focus" placeholder="your_handle"/> </div> <div class="gh-hint">Эгер Telegram көрсөтсөңүз — Emailден тышкары ошол жактан да жооп беребиз.</div> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- WhatsApp (optional) --> <div class="gh-field gh-optional" data-field="whatsapp" id="fieldWhatsApp"> <div class="gh-label-row"> <span class="gh-label">WhatsApp</span> <span class="gh-ghost">милдеттүү эмес</span> </div> <div class="gh-group"> <input id="ccode" name="ccode" type="text" value="+" readonly tabindex="-1" aria-readonly="true" class="gh-ccode gh-input-h dir-ltr"/> <input id="whatsapp" name="whatsapp" type="tel" inputmode="tel" autocomplete="tel" maxlength="24" class="gh-tel gh-input-h gh-focus dir-ltr" placeholder="380 00 000 00 00"/> </div> <div class="gh-hint dir-ltr">Формат: өлкөнүн коду жана номер (мисалы, +996XXXXXXXXX).</div> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Submit --> <div class="pt-2"> <button id="ghSubmitBtn" type="submit" class="w-full rounded-2xl border border-aqua-500/40 bg-gradient-to-r from-aqua-600 to-brand-500 px-4 py-2 text-sm font-semibold text-white hover:from-aqua-500 hover:to-brand-400 focus:outline-none focus:ring-2 focus:ring-aqua-400/50 transition-all duration-200"> Өтүнүчтү жөнөтүү </button> <p class="mt-2 text-[11px] text-slate-500">Түшүрүү баскычын басуу менен сиз маалыматтарыңыздын иштетилишине макул болосуз.</p> </div> </form> </div> </div> </div> </section> <!-- === Модалка быстрой формы === --> <div id="ghQuickModal" class="fixed inset-0 z-[90] hidden"> <!-- overlay --> <div id="ghQuickOverlay" class="absolute inset-0 bg-black/60 backdrop-blur-sm opacity-0 transition-opacity duration-200"></div> <!-- dialog --> <div class="absolute inset-0 p-4 flex items-center justify-center"> <div id="ghQuickPanel" class="relative w-full max-w-md rounded-3xl border border-white/15 bg-gradient-to-br from-night-800/80 to-night-900/80 backdrop-blur-sm opacity-0 translate-y-2 scale-[.98] transition-[opacity,transform] duration-200" style="max-height:calc(100vh - 2rem); overflow-y:auto; -webkit-overflow-scrolling:touch;"> <!-- Toast для модалки --> <div id="ghToastQuick" class="pointer-events-none absolute inset-x-4 top-4 z-20 -translate-y-2 opacity-0 transition-all duration-300"></div> <!-- header --> <div class="flex items-start justify-between p-4 pb-3"> <div> <h3 class="text-lg font-semibold text-white">Ыкчам байланыш</h3> </div> <button type="button" id="ghQuickClose" class="rounded-xl p-2 text-slate-300 hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-aqua-500/40" aria-label="Close"> <svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="m12 10.586 4.95-4.95 1.414 1.414L13.414 12l4.95 4.95-1.414 1.414L12 13.414l-4.95 4.95-1.414-1.414L10.586 12l-4.95-4.95L7.05 5.636z"/></svg> </button> </div> <!-- STATES WRAPPER --> <div class="p-4 pt-0"> <!-- FORM STATE --> <form id="ghQuickForm" class="space-y-4" autocomplete="off" novalidate> <!-- honeypot + служебные --> <input type="text" name="company" class="hidden" tabindex="-1" aria-hidden="true"> <input type="hidden" name="started_at" id="ghQuickStartedAt"> <input type="hidden" name="channel" id="ghQuickChannel" value="email"> <input type="hidden" name="subject" id="ghQuickSubject" value=""> <p class="text-xs text-slate-400 leading-relaxed"> Enter at least one contact. </p> <!-- Email --> <div class="gh-field" data-field="quick-email"> <label for="ghQuickEmail" class="block text-xs text-slate-400 mb-1">Email</label> <input id="ghQuickEmail" name="email" type="email" inputmode="email" autocomplete="email" maxlength="128" class="gh-text gh-input-h gh-focus w-full dir-ltr" placeholder="you@company.com"> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- Telegram --> <div class="gh-field" data-field="quick-telegram"> <label for="ghQuickTelegram" class="block text-xs text-slate-400 mb-1">Telegram</label> <div class="gh-group"> <span class="gh-chip gh-input-h dir-ltr" aria-hidden="true">@</span> <input id="ghQuickTelegram" name="telegram" type="text" inputmode="text" autocomplete="off" maxlength="32" class="gh-text gh-input-h gh-focus dir-ltr" placeholder="your_handle"> </div> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <!-- WhatsApp --> <div class="gh-field" data-field="quick-whatsapp"> <label for="ghQuickWhatsApp" class="block text-xs text-slate-400 mb-1">WhatsApp</label> <div class="gh-group"> <span class="gh-chip gh-input-h dir-ltr" aria-hidden="true">+</span> <input id="ghQuickWhatsApp" name="whatsapp" type="tel" inputmode="tel" autocomplete="tel" maxlength="24" class="gh-tel gh-input-h gh-focus dir-ltr" placeholder="380 00 000 00 00"> </div> <div class="gh-error-slot mt-1 text-[11px] leading-4 text-rose-300 hidden"></div> </div> <div id="ghQuickContactsError" class="hidden rounded-xl border border-rose-400/30 bg-rose-500/10 px-3 py-2 text-[11px] leading-4 text-rose-300"> </div> <!-- Кнопка --> <div class="pt-2 pb-1"> <button id="ghQuickSend" type="submit" class="w-full rounded-2xl border border-aqua-500/40 bg-gradient-to-r from-aqua-600 to-brand-500 px-4 py-2 text-sm font-semibold text-white hover:from-aqua-500 hover:to-brand-400 focus:outline-none focus:ring-2 focus:ring-aqua-400/50 transition-all duration-200"> Жөнөтүү </button> </div> </form> <!-- SUCCESS STATE --> <div id="ghQuickSuccess" class="hidden"> <div class="rounded-2xl border border-white/10 bg-white/5 p-4"> <div class="flex items-start gap-3"> <div class="shrink-0 mt-0.5"> <svg width="20" height="20" viewBox="0 0 24 24" class="opacity-90"><path fill="currentColor" d="M9.5 16.2 5.8 12.5l-1.6 1.6L9.5 20 20 9.5l-1.6-1.6z"/></svg> </div> <div class="text-sm"> <div class="font-semibold text-white">Өтүнүч жөнөтүлдү</div> <p class="mt-1 text-slate-300">Рахмат! Байланыш маалыматыңыз алынды, жакында байланышка чыгабыз. Жооп көрсөтүлгөн каналга келет.</p> </div> </div> </div> <div class="mt-3 flex gap-2"> <button id="ghQuickOk" type="button" class="flex-1 rounded-2xl border border-white/15 bg-white/10 px-4 py-2 text-sm text-white hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-aqua-400/40"> Макул / жабуу </button> <button id="ghQuickAgain" type="button" class="rounded-2xl border border-aqua-500/40 bg-gradient-to-r from-aqua-600 to-brand-500 px-4 py-2 text-sm font-semibold text-white hover:from-aqua-500 hover:to-brand-400 focus:outline-none focus:ring-2 focus:ring-aqua-400/50"> Жаңы өтүнүч </button> </div> </div> </div> <!-- /STATES WRAPPER --> </div> </div> </div> <script> /* ========= единый helper для тостов ========= */ function ghShowToastIn(container, type, text) { const el = (typeof container === 'string') ? document.getElementById(container) : container; if (!el) return; const base = 'inline-flex items-start gap-3 rounded-2xl px-4 py-3 shadow-glow border ring-1 backdrop-blur-md ' + 'absolute right-0'; const ok = 'bg-teal-600/80 border-teal-400/60 text-white'; const err = 'bg-rose-500/80 border-rose-400/60 text-white'; el.innerHTML = ` <div class="${base} ${type === 'ok' ? ok : err} dir-ltr"> <div class="mt-0.5 shrink-0"> ${ type === 'ok' ? '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M9.5 16.2 5.8 12.5l-1.6 1.6L9.5 20 20 9.5l-1.6-1.6z"/></svg>' : '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a10 10 0 1 0 .001 20.001A10 10 0 0 0 12 2m1 14h-2v-2h2zm0-4h-2V7h2z"/></svg>' } </div> <div class="text-sm leading-5">${text}</div> </div>`; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; clearTimeout(el._t); el._t = setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(-20px)'; }, 3500); } /* ========= МОДАЛКА: quick contact ========= */ document.addEventListener('DOMContentLoaded', () => { const $modal = document.getElementById('ghQuickModal'); const $overlay = document.getElementById('ghQuickOverlay'); const $panel = document.getElementById('ghQuickPanel'); const $openers = document.querySelectorAll('[data-qs-open]'); const $close = document.getElementById('ghQuickClose'); const $form = document.getElementById('ghQuickForm'); const $success = document.getElementById('ghQuickSuccess'); const $okBtn = document.getElementById('ghQuickOk'); const $againBtn = document.getElementById('ghQuickAgain'); const $email = document.getElementById('ghQuickEmail'); const $telegram = document.getElementById('ghQuickTelegram'); const $whatsapp = document.getElementById('ghQuickWhatsApp'); const $contactsError = document.getElementById('ghQuickContactsError'); const $channel = document.getElementById('ghQuickChannel'); const $subject = document.getElementById('ghQuickSubject'); const $started = document.getElementById('ghQuickStartedAt'); const $sendBtn = document.getElementById('ghQuickSend'); const emailLike = s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); const tgValid = s => /^[A-Za-z0-9_]{5,}$/.test(s); const waValid = s => /^\+\d{7,15}$/.test(s); function maskEmail(v) { v = (v || '').normalize('NFKC'); v = v.replace(/[^\x20-\x7E]/g, ''); return v.replace(/\s+/g, '').slice(0, 128); } function maskTelegram(v) { v = (v || '').replace(/^@+/, ''); return v.replace(/[^A-Za-z0-9_]/g, '').slice(0, 32); } function maskWhatsapp(v) { const digits = (v || '').replace(/\D/g, '').slice(0, 15); const groups = []; for (let i = 0; i < digits.length; i += 3) { groups.push(digits.slice(i, i + 3)); } return groups.join(' '); } function qGetSlot($el) { const wrap = $el.closest('.gh-field') || $el.parentElement; let slot = wrap.querySelector('.gh-error-slot'); if (!slot) { slot = document.createElement('div'); slot.className = 'gh-error-slot mt-1 text-[11px] leading-4 text-rose-300'; wrap.appendChild(slot); } return slot; } function qMarkError($el, msg) { if (!$el) return; $el.classList.add('gh-error'); $el.setAttribute('aria-invalid', 'true'); const slot = qGetSlot($el); slot.textContent = msg || ''; slot.classList.remove('hidden'); } function qClearError($el) { if (!$el) return; $el.classList.remove('gh-error'); $el.removeAttribute('aria-invalid'); const wrap = $el.closest('.gh-field') || $el.parentElement; const slot = wrap.querySelector('.gh-error-slot'); if (slot) { slot.textContent = ''; slot.classList.add('hidden'); } } function clearContactsError() { if (!$contactsError) return; $contactsError.textContent = ''; $contactsError.classList.add('hidden'); } function showContactsError(text) { if (!$contactsError) return; $contactsError.textContent = text; $contactsError.classList.remove('hidden'); } function clearAllQuickErrors() { [$email, $telegram, $whatsapp].forEach(qClearError); clearContactsError(); } function clearQuickContacts() { if ($email) $email.value = ''; if ($telegram) $telegram.value = ''; if ($whatsapp) $whatsapp.value = ''; clearAllQuickErrors(); } function showFormState() { $success.classList.add('hidden'); $form.classList.remove('hidden'); $sendBtn.disabled = false; $sendBtn.innerHTML = 'Жөнөтүү'; $started.value = String(Date.now()); setTimeout(() => $email.focus(), 60); } function showSuccessState() { $form.classList.add('hidden'); $success.classList.remove('hidden'); setTimeout(() => $okBtn.focus(), 30); } function openModal(subj) { clearQuickContacts(); $subject.value = subj || 'Quick Request'; $channel.value = 'email'; $started.value = String(Date.now()); showFormState(); $modal.classList.remove('hidden'); requestAnimationFrame(() => { $overlay.classList.remove('opacity-0'); $panel.classList.remove('opacity-0', 'translate-y-2', 'scale-[.98]'); }); setTimeout(() => $email.focus(), 120); } function closeModal() { $overlay.classList.add('opacity-0'); $panel.classList.add('opacity-0', 'translate-y-2', 'scale-[.98]'); setTimeout(() => { $modal.classList.add('hidden'); }, 200); } $openers.forEach(el => { el.addEventListener('click', e => { e.preventDefault(); openModal(el.getAttribute('data-qs-subject')); }); }); $close.addEventListener('click', closeModal); $okBtn.addEventListener('click', closeModal); $againBtn.addEventListener('click', () => { clearQuickContacts(); showFormState(); }); $modal.addEventListener('click', e => { if (e.target === $modal) closeModal(); }); document.addEventListener('keydown', e => { if (!$modal.classList.contains('hidden') && e.key === 'Escape') { closeModal(); } }); $email.addEventListener('input', () => { $email.value = maskEmail($email.value); qClearError($email); clearContactsError(); }); $telegram.addEventListener('input', () => { $telegram.value = maskTelegram($telegram.value); qClearError($telegram); clearContactsError(); }); $whatsapp.addEventListener('input', () => { $whatsapp.value = maskWhatsapp($whatsapp.value); qClearError($whatsapp); clearContactsError(); }); [$email, $telegram, $whatsapp].forEach($el => { $el.addEventListener('focus', () => { qClearError($el); clearContactsError(); }); }); $form.addEventListener('submit', async e => { e.preventDefault(); clearAllQuickErrors(); const emailVal = maskEmail($email.value.trim()); const telegramHandle = maskTelegram($telegram.value.trim()); const whatsappDigits = $whatsapp.value.replace(/\D/g, '').slice(0, 15); $email.value = emailVal; $telegram.value = telegramHandle; $whatsapp.value = maskWhatsapp(whatsappDigits); const hasEmail = emailVal.length > 0; const hasTelegram = telegramHandle.length > 0; const hasWhatsApp = whatsappDigits.length > 0; if (!hasEmail && !hasTelegram && !hasWhatsApp) { const text = 'Enter at least one contact: email, Telegram or WhatsApp.'; showContactsError(text); ghShowToastIn('ghToastQuick', 'err', text); return; } let hasError = false; if (hasEmail && !emailLike(emailVal)) { qMarkError($email, 'Invalid email'); hasError = true; } if (hasTelegram && !tgValid(telegramHandle)) { qMarkError($telegram, 'Only letters, digits and _ (min 5)'); hasError = true; } const whatsappFull = hasWhatsApp ? ('+' + whatsappDigits) : ''; if (hasWhatsApp && !waValid(whatsappFull)) { qMarkError($whatsapp, 'Invalid WhatsApp number'); hasError = true; } if (hasError) { ghShowToastIn('ghToastQuick', 'err', 'Check the contact details.'); return; } const primaryChannel = hasEmail ? 'email' : (hasTelegram ? 'telegram' : 'whatsapp'); $channel.value = primaryChannel; const fd = new FormData(); fd.append('started_at', $started.value || String(Date.now())); fd.append('company', ''); fd.append('channel', primaryChannel); fd.append('subject', $subject.value || 'Quick Request'); fd.append( 'message', 'Quick contact request.\nAt least one contact method was provided.\n\nThis message was sent from the quick modal.' ); if (hasEmail) fd.append('email', emailVal); if (hasTelegram) fd.append('telegram', '@' + telegramHandle); if (hasWhatsApp) fd.append('whatsapp_full', whatsappFull); const old = $sendBtn.innerHTML; $sendBtn.disabled = true; $sendBtn.innerHTML = 'Sending…'; try { const res = await fetch('/ajax/contact.php', { method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd, credentials: 'same-origin' }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success) { if (res.status === 429 && data.retry_after) { ghShowToastIn( 'ghToastQuick', 'err', data.error_human || ('Please wait ' + data.retry_after + 's') ); } else { ghShowToastIn( 'ghToastQuick', 'err', (data && (data.error_human || data.error)) || ('HTTP ' + res.status) ); } return; } ghShowToastIn('ghToastQuick', 'ok', 'Өтүнүч жөнөтүлдү. Жакынкы арада сиз менен байланышабыз.'); clearQuickContacts(); showSuccessState(); } catch (error) { ghShowToastIn('ghToastQuick', 'err', 'Network error. Try again later.'); } finally { $sendBtn.disabled = false; $sendBtn.innerHTML = old; } }); $started.value = String(Date.now()); }); /* ========= ОСНОВНАЯ ФОРМА ========= */ document.addEventListener('DOMContentLoaded', () => { const $form = document.getElementById('ghContactForm'); const $btn = document.getElementById('ghSubmitBtn'); const $name = document.getElementById('name'); const $email = document.getElementById('email'); const $subject = document.getElementById('subject'); const $message = document.getElementById('message'); const $tg = document.getElementById('telegram'); const $ccode = document.getElementById('ccode'); const $whats = document.getElementById('whatsapp'); const $waFull = document.getElementById('ghWaFull'); const $channel = document.getElementById('ghChannel'); const $started = document.getElementById('ghStartedAt'); const asciiOnly = s => s.replace(/[^\x20-\x7E]/g,''); const emailLike = s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); const tgValid = s => /^[A-Za-z0-9_]{5,}$/.test(s); const onlyDigits = s => s.replace(/\D+/g,''); const phoneFullValid = s => /^\+\d{7,15}$/.test(s); function getSlot($el) { const wrap = $el.closest('.gh-field') || $el.parentElement; let slot = wrap.querySelector('.gh-error-slot'); if (!slot) { slot = document.createElement('div'); slot.className = 'gh-error-slot mt-1 text-[11px] leading-4 text-rose-300'; wrap.appendChild(slot); } return slot; } function markError($el, msg) { if (!$el) return; $el.classList.add('gh-error'); $el.setAttribute('aria-invalid','true'); const slot = getSlot($el); slot.textContent = msg; slot.classList.remove('hidden'); } function clearError($el) { if (!$el) return; $el.classList.remove('gh-error'); $el.removeAttribute('aria-invalid'); const wrap = $el.closest('.gh-field') || $el.parentElement; const slot = wrap.querySelector('.gh-error-slot'); if (slot) { slot.textContent = ''; slot.classList.add('hidden'); } } [$name,$email,$subject,$message,$tg,$ccode,$whats].forEach(($el)=>{ if(!$el) return; $el.addEventListener('input', () => clearError($el)); $el.addEventListener('focus', () => clearError($el)); }); (function(){ const optWraps = document.querySelectorAll('.gh-optional'); const digits = s => (s||'').replace(/\D+/g,''); const refresh = (wrap)=>{ const hasFocus = document.activeElement && wrap.contains(document.activeElement); let hasVal = false; if (wrap.id === 'fieldWhatsApp') { const tel = wrap.querySelector('#whatsapp'); hasVal = digits(tel && tel.value).length > 0; } else if (wrap.id === 'fieldTelegram') { const tg = wrap.querySelector('#telegram'); hasVal = !!(tg && tg.value && tg.value.trim().length > 0); } else { const inputs = wrap.querySelectorAll('input,textarea'); hasVal = Array.from(inputs).some(i => (i.value||'').trim().length>0); } wrap.classList.toggle('is-active', hasFocus || hasVal); }; optWraps.forEach(wrap=>{ refresh(wrap); wrap.addEventListener('focusin', ()=>refresh(wrap)); wrap.addEventListener('focusout', ()=>setTimeout(()=>refresh(wrap), 10)); wrap.querySelectorAll('input,textarea').forEach(i=>{ i.addEventListener('input', ()=>refresh(wrap)); i.addEventListener('change', ()=>refresh(wrap)); i.addEventListener('keyup', ()=>refresh(wrap)); }); }); })(); if ($started) $started.value = String(Date.now()); if ($ccode) { $ccode.value = '+'; $ccode.readOnly = true; $ccode.setAttribute('aria-readonly','true'); $ccode.setAttribute('tabindex','-1'); } if ($whats) { $whats.setAttribute('autocomplete','tel'); $whats.maxLength = 24; $whats.addEventListener('input', () => { let v = $whats.value.replace(/[^\d()\-\s]/g, ''); v = v.replace(/\s{2,}/g, ' '); $whats.value = v.slice(0, 24); }); } if ($email) { $email.setAttribute('autocomplete','email'); $email.maxLength = 128; $email.addEventListener('input', () => { let v = $email.value.replace(/[^\x20-\x7E]/g, ''); v = v.replace(/\s+/g, ''); $email.value = v; }); } if ($tg) { $tg.setAttribute('autocomplete','off'); $tg.maxLength = 32; $tg.addEventListener('input', () => { let v = $tg.value.trim(); if (v.startsWith('@')) v = v.slice(1); v = v.replace(/[^A-Za-z0-9_]/g, ''); $tg.value = v; }); } $form.addEventListener('submit', async (e) => { e.preventDefault(); [$name,$email,$subject,$message,$tg,$ccode,$whats].forEach(clearError); const nameVal = ($name.value || '').trim(); const emailVal = asciiOnly(($email.value || '').trim()); const subjVal = ($subject.value || '').trim(); const msgVal = ($message.value || '').trim(); let tgVal = ($tg ? $tg.value.trim() : ''); if (tgVal.startsWith('@')) tgVal = tgVal.slice(1); tgVal = tgVal.replace(/[^A-Za-z0-9_]/g,''); const telDigits = onlyDigits($whats ? $whats.value : ''); const waFull = '+' + telDigits; if (nameVal.length < 2) { markError($name,'Enter your name (min 2 chars)'); ghShowToastIn('ghToastMain','err','Please enter your name.'); return; } if (!emailLike(emailVal)) { markError($email,'Invalid email'); ghShowToastIn('ghToastMain','err','Invalid email.'); return; } if (subjVal.length < 2) { markError($subject,'Subject is required'); ghShowToastIn('ghToastMain','err','Please specify a subject.'); return; } if (msgVal.length < 5) { markError($message,'Message is too short'); ghShowToastIn('ghToastMain','err','Message is too short.'); return; } if ($whats && telDigits.length > 0) { if (!phoneFullValid(waFull)) { markError($whats,'Invalid phone format'); ghShowToastIn('ghToastMain','err','Invalid phone number.'); return; } } if ($tg && tgVal && !tgValid(tgVal)) { markError($tg,'Only letters, digits and _ (min 5)'); ghShowToastIn('ghToastMain','err','Invalid Telegram handle.'); return; } if ($waFull) $waFull.value = (telDigits.length ? waFull : ''); if ($channel) $channel.value = 'email'; if ($started && !$started.value) $started.value = String(Date.now()); const fd = new FormData($form); fd.set('telegram', tgVal ? ('@' + tgVal) : ''); const oldHtml = $btn.innerHTML; $btn.disabled = true; $btn.innerHTML = 'Жөнөтүлүүдө…'; try { const res = await fetch('/ajax/contact.php', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd, credentials: 'same-origin' }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.success) { const msg = (data && (data.error_human || data.error)) ? (data.error_human || data.error) : ('HTTP ' + res.status); if (res.status === 429 && data.retry_after) { ghShowToastIn('ghToastMain','err', data.error_human || ('Please wait '+data.retry_after+'s')); } else { ghShowToastIn('ghToastMain','err', msg || 'Send error'); } if (msg === 'Name required') markError($name, 'Name required'); if (msg === 'Invalid email') markError($email, 'Invalid email'); if (msg === 'Invalid telegram') markError($tg, 'Invalid telegram'); if (msg === 'Invalid phone') markError($whats, 'Invalid phone'); return; } ghShowToastIn('ghToastMain','ok','Өтүнүч ийгиликтүү жөнөтүлдү. Жакынкы арада сиз менен байланышабыз.'); $form.reset(); if ($waFull) $waFull.value = ''; if ($channel) $channel.value = 'email'; if ($ccode) $ccode.value = '+'; document.querySelectorAll('.gh-optional').forEach(w=>w.classList.remove('is-active')); } catch (err) { ghShowToastIn('ghToastMain','err','Network error. Try again later.'); } finally { $btn.disabled = false; $btn.innerHTML = oldHtml; } }); }); </script> </main> <style> /* =================== GAMBLEHUB FOOTER =================== */ .gh-footer { position: relative; border-top: 1px solid rgba(255, 255, 255, 0.10); background: linear-gradient( 180deg, rgba(7, 13, 25, 0.82) 0%, rgba(5, 10, 20, 0.96) 100% ); } .gh-footer::before { content: ""; position: absolute; top: -1px; left: 50%; width: min(34rem, 72%); height: 1px; transform: translateX(-50%); pointer-events: none; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.16), transparent ); } .gh-footer-inner { width: 100%; max-width: 80rem; margin: 0 auto; padding: 2.5rem 1rem 1.25rem; } /* =================== ОСНОВНОЙ БЛОК =================== */ .gh-footer-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; } /* =================== ЛОГО И ОПИСАНИЕ =================== */ .gh-footer-brand { min-width: 0; } .gh-footer-logo { display: inline-flex; align-items: center; max-width: 100%; text-decoration: none; } .gh-footer-logo img { display: block; width: 150px; height: auto; max-width: 100%; } .gh-footer-slogan { max-width: 27rem; margin: 1rem 0 0; color: #8491a4; font-size: 0.8125rem; line-height: 1.65; } /* =================== МЕНЮ =================== */ .gh-footer-menus { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; min-width: 0; } .gh-footer-column { min-width: 0; } .gh-footer-title { margin: 0 0 0.875rem; color: #e2e8f0; font-size: 0.875rem; line-height: 1.35; font-weight: 700; } /* * Нормализация HTML из BottomMenu() */ .gh-footer-menu, .gh-footer-menu ul, .gh-footer-menu ol { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; width: 100%; min-width: 0; margin: 0; padding: 0; list-style: none; } .gh-footer-menu li { width: 100%; min-width: 0; margin: 0; padding: 0; list-style: none; } .gh-footer-menu a { position: relative; display: inline-flex; align-items: center; max-width: 100%; color: #94a3b8; font-size: 0.8125rem; line-height: 1.5; text-decoration: none !important; overflow-wrap: anywhere; transition: color 0.2s ease; } .gh-footer-menu a:hover { color: #e7fbff; } /* Активный пункт меню */ .gh-footer-menu .gh-bottom-menu-link.is-active { color: #e7fbff; } .gh-footer-menu .gh-bottom-menu-link.is-active::after { content: ""; position: absolute; right: 0; bottom: -3px; left: 0; height: 1px; border-radius: 999px; background: rgba(231, 251, 255, 0.68); pointer-events: none; } /* =================== НИЖНЯЯ СТРОКА =================== */ .gh-footer-bottom { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid rgba(255, 255, 255, 0.075); } .gh-footer-copy, .gh-footer-created { margin: 0; color: #64748b; font-size: 0.75rem; line-height: 1.55; text-align: center; } .gh-footer-created strong { color: #a9b5c5; font-weight: 600; } /* =================== УЗКАЯ МОБИЛКА =================== */ @media (max-width: 420px) { .gh-footer-inner { padding-top: 2rem; } .gh-footer-main { gap: 1.75rem; } .gh-footer-menus { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; } .gh-footer-logo img { width: 145px; } .gh-footer-slogan { margin-top: 0.875rem; font-size: 0.8125rem; } .gh-footer-title { margin-bottom: 0.75rem; font-size: 0.9375rem; } .gh-footer-menu a { font-size: 0.8125rem; } } /* =================== ПЛАНШЕТ =================== */ @media (min-width: 640px) { .gh-footer-inner { padding-right: 1.5rem; padding-left: 1.5rem; } .gh-footer-main { grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr); align-items: start; gap: 3rem; } .gh-footer-menus { gap: 2.5rem; } .gh-footer-bottom { flex-direction: row; justify-content: space-between; gap: 1rem; } .gh-footer-copy { text-align: left; } .gh-footer-created { text-align: right; } } /* =================== ДЕСКТОП =================== */ @media (min-width: 1024px) { .gh-footer-inner { padding: 2.75rem 2rem 1.25rem; } .gh-footer-main { grid-template-columns: minmax(280px, 0.85fr) minmax(0, 1.15fr); gap: 4.5rem; } .gh-footer-brand { padding-right: 3rem; border-right: 1px solid rgba(255, 255, 255, 0.075); } .gh-footer-menus { gap: 4rem; } } /* =================== RTL =================== */ [dir="rtl"] .gh-footer-menu, [dir="rtl"] .gh-footer-menu ul, [dir="rtl"] .gh-footer-menu ol { align-items: flex-start; } @media (min-width: 640px) { [dir="rtl"] .gh-footer-copy { text-align: right; } [dir="rtl"] .gh-footer-created { text-align: left; } } @media (min-width: 1024px) { [dir="rtl"] .gh-footer-brand { padding-right: 0; padding-left: 3rem; border-right: 0; border-left: 1px solid rgba(255, 255, 255, 0.075); } } @media (prefers-reduced-motion: reduce) { .gh-footer-menu a { transition: none; } } </style> <!-- FOOTER --> <footer class="gh-footer"> <div class="gh-footer-inner"> <div class="gh-footer-main"> <!-- ЛОГО И ОПИСАНИЕ --> <div class="gh-footer-brand"> <a href="/ky/" class="gh-footer-logo" aria-label="GambleHub" > <img src="/images/logo.svg" width="150" height="29" alt="GambleHub" loading="lazy" decoding="async" > </a> <p class="gh-footer-slogan"> GambleHub слотторду, пазлдарды, чеберчиликке негизделген оюндарды жана виртуалдык спорт түрлөрүн камтыган оюндардын кеңири топтомун сунуштайт. </p> </div> <!-- МЕНЮ --> <div class="gh-footer-menus"> <!-- COMPANY --> <div class="gh-footer-column"> <div class="gh-footer-title"> Компания </div> <div class="gh-footer-menu"> <a href="/ky/about-us/" class="gh-bottom-menu-link">Биз жөнүндө</a><a href="/ky/services/" class="gh-bottom-menu-link">Кызмат көрсөтүүлөр</a><a href="/ky/features/" class="gh-bottom-menu-link">Мүмкүнчүлүктөр</a><a href="/ky/partners/" class="gh-bottom-menu-link">Өнөктөштөр</a><a href="/ky/contact-us/" class="gh-bottom-menu-link">Байланыштар</a> </div> </div> <!-- LEGAL --> <div class="gh-footer-column"> <div class="gh-footer-title"> Юридикалык маалымат </div> <div class="gh-footer-menu"> <a href="/ky/responsible-gaming/" class="gh-bottom-menu-link">Жооптуу оюн</a><a href="/ky/gaming-license/" class="gh-bottom-menu-link">Оюн лицензиясы</a><a href="/ky/privacy-policy/" class="gh-bottom-menu-link">Купуялык саясаты</a><a href="/ky/terms-and-conditions/" class="gh-bottom-menu-link">Колдонуу шарттары</a><a href="/ky/cookie-policy/" class="gh-bottom-menu-link">Cookie саясаты</a> </div> </div> </div> </div> <!-- НИЖНЯЯ СТРОКА --> <div class="gh-footer-bottom dir-ltr"> <p class="gh-footer-copy"> Copyright © GambleHub. 2024 ® All Rights Reserved. </p> <p class="gh-footer-created"> Created by <strong>GambleHub</strong> </p> </div> </div> </footer> <!-- Единственный JS: мобильное меню --> <script> (function(){ const btn = document.getElementById('navToggle'); const panel = document.getElementById('navPanel'); if(!btn || !panel) return; // БЕЗ блокировки фона — ничего не трогаем у <html> const isOpen = () => panel.classList.contains('active'); const open = () => panel.classList.add('active'); const close = () => panel.classList.remove('active'); // Бургер — переключение btn.addEventListener('click', (e)=>{ e.stopPropagation(); isOpen() ? close() : open(); }); // Клик внутри меню — не закрывает panel.addEventListener('click', e => e.stopPropagation()); // Клик вне меню/кнопки — закрывает document.addEventListener('click', (e)=>{ if (isOpen() && !panel.contains(e.target) && !btn.contains(e.target)) close(); }); // Esc — закрыть document.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && isOpen()) close(); }); })(); </script> <script> (function(){ const RECENT_LIMIT = 6; const STORE_KEY = 'lang_recent_codes'; function uid(){ return Math.random().toString(36).slice(2,8); } function readRecent(){ try { return JSON.parse(localStorage.getItem(STORE_KEY) || '[]'); } catch(e){ return []; } } function pushRecent(code){ const arr = readRecent().filter(c=>c!==code); arr.unshift(code); localStorage.setItem(STORE_KEY, JSON.stringify(arr.slice(0, RECENT_LIMIT))); } function closeAll(except){ document.querySelectorAll('.language-menu.show').forEach(m=>{ if (m!==except) m.classList.remove('show'); }); } // Делегирование document.addEventListener('click', (e)=>{ const wrap = e.target.closest('.language-selector'); const isBtn = e.target.closest('.language-button'); if (wrap && isBtn){ const menu = wrap.querySelector('.language-menu'); if (!menu) return; closeAll(menu); menu.classList.toggle('show'); const input = menu.querySelector('.language-search input'); if (menu.classList.contains('show') && input){ setTimeout(()=>input.focus(), 0); } return; } // Выбор языка const link = e.target.closest('.language-menu a[data-lang]'); if (link){ const code = link.getAttribute('data-lang'); if (code) pushRecent(code); // стандартный переход по href произойдет сам return; } // Клик вне — закрыть if (!e.target.closest('.language-menu')) closeAll(); }); // Клавиатура: Esc/стрелки/Home/End/Enter/Space document.addEventListener('keydown', (e)=>{ const openMenu = document.querySelector('.language-menu.show'); if (!openMenu) return; const focusables = [...openMenu.querySelectorAll('a[data-lang]')]; const idx = focusables.indexOf(document.activeElement); if (e.key === 'Escape'){ closeAll(); return; } if (e.key === 'ArrowDown'){ e.preventDefault(); (focusables[idx+1] || focusables[0])?.focus(); } if (e.key === 'ArrowUp'){ e.preventDefault(); (focusables[idx-1] || focusables[focusables.length-1])?.focus(); } if (e.key === 'Home'){ e.preventDefault(); focusables[0]?.focus(); } if (e.key === 'End'){ e.preventDefault(); focusables[focusables.length-1]?.focus(); } if (e.key === ' ' || e.key === 'Enter'){ if (document.activeElement?.tagName === 'A'){ document.activeElement.click(); } } }); // Живой поиск document.addEventListener('input', (e)=>{ const input = e.target.closest('.language-search input'); if (!input) return; const menu = input.closest('.language-menu'); if (!menu) return; const q = input.value.trim().toLowerCase(); menu.querySelectorAll('a[data-lang]').forEach(a=>{ const hay = (a.dataset.search || '').toLowerCase(); a.style.display = hay.includes(q) ? '' : 'none'; }); }); // Рендер «Недавние» (когда меню впервые открывается) document.addEventListener('click', (e)=>{ const btn = e.target.closest('.language-button'); if (!btn) return; const menu = btn.parentElement.querySelector('.language-menu'); if (!menu) return; const recentWrap = menu.querySelector('[data-recent]'); if (!recentWrap) return; if (recentWrap.dataset.inited === '1') return; const recent = readRecent(); if (!recent.length){ recentWrap.remove(); return; } // Показать только те, что реально есть в меню const map = {}; menu.querySelectorAll('a[data-lang]').forEach(a=>{ map[a.dataset.lang] = a.outerHTML; }); const inner = recent.map(code=>map[code]).filter(Boolean).join(''); if (inner){ recentWrap.insertAdjacentHTML('beforeend', inner); recentWrap.dataset.inited = '1'; } else { recentWrap.remove(); } }); })(); </script> <script> (function(){ const pillsWrap = document.querySelector('.filter-pills'); const gridWrap = document.getElementById('providersGrid'); if (!pillsWrap || !gridWrap) return; const pills = Array.from(pillsWrap.querySelectorAll('[data-cat]')); const px = (gridWrap.getAttribute('data-px') || '').trim(); const url = (gridWrap.getAttribute('data-url') || '').trim(); let current = 9; let inflight; // ---------- helpers ---------- function setActive(catId){ pills.forEach(btn => { const on = parseInt(btn.dataset.cat,10) === catId; btn.classList.toggle('is-active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); btn.setAttribute('role', 'tab'); }); } function gridColsClass(){ return 'providers-grid mt-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3'; } // Берём ТОЛЬКО основной грид — прямой ребёнок providersGrid function getMainGrid(){ // :scope гарантирует, что не схватим UL внутри модалки return gridWrap.querySelector(':scope > ul.providers-grid'); } // скелетон — только внутри основного UL function renderSkeleton(count = 8){ const items = Array.from({length: count}) .map(() => '<li class="gh-skel gh-skel-card"></li>') .join(''); let ul = getMainGrid(); if (!ul) { ul = document.createElement('ul'); ul.className = gridColsClass(); gridWrap.appendChild(ul); } ul.innerHTML = items; } function ensureOverlay(){ let ov = gridWrap.querySelector('.gh-preload__overlay'); if (!ov) { ov = document.createElement('div'); ov.className = 'gh-preload__overlay'; ov.setAttribute('aria-hidden', 'true'); ov.innerHTML = '<div class="gh-spinner" role="progressbar" aria-label="Loading"></div>'; gridWrap.appendChild(ov); } return ov; } async function loadCat(catId){ if (inflight) inflight.abort(); inflight = new AbortController(); setActive(catId); gridWrap.setAttribute('aria-busy','true'); renderSkeleton(8); const overlay = ensureOverlay(); overlay.classList.add('is-visible'); const started = performance.now(); const minShow = 450; try{ const body = new URLSearchParams({ id: String(catId), px, url}); const resp = await fetch('/ajax/providers.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body, signal: inflight.signal }); if(!resp.ok) throw new Error('Network error'); const html = await resp.text(); const elapsed = performance.now() - started; if (elapsed < minShow) { await new Promise(r => setTimeout(r, minShow - elapsed)); } // Меняем только основной UL, модалку не трогаем const oldGrid = getMainGrid(); if (oldGrid) { // html = <ul class="providers-grid ...">...</ul> oldGrid.outerHTML = html; } else { // на всякий случай, если по каким-то причинам UL ещё нет gridWrap.insertAdjacentHTML('beforeend', html); } } catch(err){ if (err.name === 'AbortError') return; console.error(err); gridWrap.innerHTML = ` <div class="mt-10 text-center text-slate-300"> <div class="inline-block border border-white/10 rounded-2xl px-6 py-8 bg-white/5 max-w-md"> <p class="text-lg font-semibold text-white"> Ошибка загрузки </p> <p class="text-sm mt-1 text-slate-400"> Пожалуйста, попробуйте ещё раз </p> </div> </div>`; } finally { gridWrap.removeAttribute('aria-busy'); const overlay = gridWrap.querySelector('.gh-preload__overlay'); if (overlay) overlay.classList.remove('is-visible'); } } // ---------- events ---------- pillsWrap.addEventListener('click', (e) => { const btn = e.target.closest('[data-cat]'); if (!btn) return; const catId = parseInt(btn.dataset.cat, 10); if (catId === current) return; current = catId; loadCat(current); }); setActive(current); })(); </script> <script> document.addEventListener('DOMContentLoaded', () => { const io = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('is-inview'); obs.unobserve(entry.target); } }); }, { threshold: 0.3 }); document.querySelectorAll('[data-reveal] .reveal-item').forEach(el => io.observe(el)); }); </script> <script> // === Smooth scroll with offset (GambleHub style) === document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { const targetId = this.getAttribute('href').substring(1); const target = document.getElementById(targetId); if (target) { e.preventDefault(); // Смещение в пикселях (можно менять) const offset = 70; // Расчёт позиции с учётом текущего скролла const y = target.getBoundingClientRect().top + window.scrollY - offset; // Плавный скролл window.scrollTo({ top: y, behavior: 'smooth' }); // Добавим лёгкую подсветку цели (опционально) target.classList.add('ring-1', 'ring-aqua-400/40', 'transition'); setTimeout(() => target.classList.remove('ring-1', 'ring-aqua-400/40'), 1000); } }); }); </script> <script> document.querySelectorAll('a, button').forEach(el => { el.addEventListener('click', () => { if (typeof gtag === 'function') { gtag('event', 'click', { event_category: 'interaction', event_label: el.dataset.role || el.dataset.href || 'unknown', value: 1 }); } }); }); </script> <!-- ===================== VIDEO MODAL ===================== --> <div id="ghVideoModal" class="fixed inset-0 z-[90] hidden items-center justify-center p-4 bg-black/70 backdrop-blur-sm transition-opacity duration-300 opacity-0"> <!-- overlay закрытия --> <div class="absolute inset-0" onclick="ghCloseVideoModal()"></div> <!-- content --> <div id="ghVideoModalContent" class="relative w-full max-w-md rounded-3xl bg-night-900 border border-white/10 p-8 gh-glow text-center transform scale-95 opacity-0 transition-all duration-300 ease-out"> <!-- YouTube icon --> <div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-3xl bg-red-600/20 text-red-400 shadow-[0_0_30px_rgba(255,0,0,.25)]"> <svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" fill="currentColor" viewBox="0 0 16 16"> <path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.01 2.01 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.01 2.01 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31 31 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.01 2.01 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A100 100 0 0 1 7.858 2zM6.4 5.209v4.818l4.157-2.408z"/> </svg> </div> <!-- Title --> <h2 class="text-lg font-semibold text-white mb-2"> Видео жакында жаңыртылат </h2> <!-- Text --> <p class="text-slate-400 text-sm leading-relaxed mb-6"> Видео учурда жаңыртылып жатат.<br>Сураныч, кийинчерээк кайра кириңиз — жаңы версия жолдо. </p> <!-- Close button --> <button onclick="ghCloseVideoModal()" class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-slate-200 hover:border-white/20 hover:bg-white/10 transition"> Жабуу </button> </div> </div> <!-- ===================== / VIDEO MODAL ===================== --> <script> function ghOpenVideoModal() { const modal = document.getElementById('ghVideoModal'); const content = document.getElementById('ghVideoModalContent'); modal.classList.remove('hidden'); modal.classList.add('flex'); // блокируем скролл document.body.style.overflow = "hidden"; // запускаем анимацию (fade + scale) setTimeout(() => { modal.classList.remove('opacity-0'); content.classList.remove('opacity-0', 'scale-95'); }, 20); } function ghCloseVideoModal() { const modal = document.getElementById('ghVideoModal'); const content = document.getElementById('ghVideoModalContent'); // анимация скрытия modal.classList.add('opacity-0'); content.classList.add('opacity-0', 'scale-95'); // ждём 300ms пока анимация пойдёт setTimeout(() => { modal.classList.add('hidden'); modal.classList.remove('flex'); document.body.style.overflow = ""; }, 300); } </script> <!-- ===================== SOCIAL MODAL ===================== --> <div id="ghSocialModal" class="fixed inset-0 z-[90] hidden items-center justify-center p-4 bg-black/70 backdrop-blur-sm transition-opacity duration-300 opacity-0"> <!-- overlay --> <div class="absolute inset-0" onclick="ghCloseSocialModal()"></div> <!-- content --> <div id="ghSocialModalContent" class="relative w-full max-w-md rounded-3xl bg-night-900 border border-white/10 p-8 gh-glow text-center transform scale-95 opacity-0 transition-all duration-300 ease-out"> <!-- icons row — теперь компактный, как YouTube-иконка по смыслу --> <div class="flex items-center justify-center gap-3 mb-6"> <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5 text-slate-200 shadow-[0_0_18px_rgba(255,255,255,.08)]"> <!-- X --> <svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <path d="M3 3l18 18M21 3L3 21" stroke="currentColor" stroke-width="1.6"/> <path d="M14.5 3h3.9L12.9 11l6.1 10h-3.9L12 14.6 8.9 21H5l6.1-10L5 3h3.9L12 9.4 14.5 3z" fill="currentColor" opacity=".9"/> </svg> </div> <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5 text-slate-200 shadow-[0_0_18px_rgba(255,255,255,.08)]"> <!-- LinkedIn --> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <path d="M4.98 3.5C4.98 4.88 3.86 6 2.5 6S0 4.88 0 3.5 1.12 1 2.5 1s2.48 1.12 2.48 2.5zM0 8.5h5V23H0V8.5zM8 8.5h4.8v2h.07c.67-1.2 2.3-2.46 4.73-2.46C21.86 8.04 24 10 24 13.86V23h-5v-7.7c0-1.84-.03-4.2-2.56-4.2-2.56 0-2.96 2-2.96 4.06V23H8V8.5z"/> </svg> </div> <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5 text-slate-200 shadow-[0_0_18px_rgba(255,255,255,.08)]"> <!-- YouTube --> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <path d="M23.5 6.2a3.4 3.4 0 0 0-2.4-2.4C19.2 3.3 12 3.3 12 3.3s-7.2 0-9.1.5A3.4 3.4 0 0 0 .5 6.2C0 8.1 0 12 0 12s0 3.9.5 5.8a3.4 3.4 0 0 0 2.4 2.4c1.9.5 9.1.5 9.1.5s7.2 0 9.1-.5a3.4 3.4 0 0 0 2.4-2.4c.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8ZM9.6 15.5v-7l6.2 3.5-6.2 3.5Z"/> </svg> </div> </div> <!-- Title --> <h2 class="text-lg font-semibold text-white mb-3"> Азыр биз долбоорлор менен абдан алекпиз </h2> <!-- Text --> <p class="text-slate-400 text-sm leading-relaxed mb-6"> Учурда биздин толук көңүл бурууңуздардын продукцияларын иштеп чыгууга жана интеграциялоого багытталган.<br><br>Бирок биз социалдык тармактарга да убакыт табууга аракет кылабыз. Жакында мазмунду жаңылайбыз — көңүлүңүздөрдү буруп туруңуздар! </p> <!-- Close button --> <button onclick="ghCloseSocialModal()" class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-slate-200 hover:border-white/20 hover:bg-white/10 transition"> Жабуу </button> </div> </div> <!-- ===================== / SOCIAL MODAL ===================== --> <script> function ghOpenSocialModal() { const modal = document.getElementById('ghSocialModal'); const content = document.getElementById('ghSocialModalContent'); modal.classList.remove('hidden'); modal.classList.add('flex'); document.body.style.overflow = "hidden"; setTimeout(() => { modal.classList.remove('opacity-0'); content.classList.remove('opacity-0', 'scale-95'); }, 20); } function ghCloseSocialModal() { const modal = document.getElementById('ghSocialModal'); const content = document.getElementById('ghSocialModalContent'); modal.classList.add('opacity-0'); content.classList.add('opacity-0', 'scale-95'); setTimeout(() => { modal.classList.add('hidden'); modal.classList.remove('flex'); document.body.style.overflow = ""; }, 300); } </script> </body> </html>