1function getCSRFToken() { 2 const name = 'csrftoken='; 3 const decodedCookie = decodeURIComponent(document.cookie); 4 const cookies = decodedCookie.split(';'); 5 for (let c of cookies) { 6 c = c.trim(); 7 if (c.indexOf(name) === 0) { 8 return c.substring(name.length, c.length); 9 } 10 } 11 return ''; 12} 13 14window.api = axios.create({ 15 baseURL: '/', 16 headers: { 17 'X-CSRFToken': getCSRFToken(), 18 }, 19}); 20 21// ?next=/secretadmin/ 22 23// /s/auth/login/?next= 24window.api.interceptors.request.use((config) => { 25 if (config.data instanceof FormData) { 26 delete config.headers['Content-Type']; 27 } else { 28 config.headers['Content-Type'] = 'application/json'; 29 } 30 31 return config; 32}); 33 34window.api.interceptors.response.use( 35 (res) => res, 36 (error) => { 37 const status = error?.response?.status; 38 if (status === 401 || status === 403) { 39 const loginPath = '/s/auth/login/'; 40 41 const current = window.location.pathname + window.location.search + window.location.hash; 42 43 const alreadyOnLogin = window.location.pathname.startsWith(loginPath); 44 if (!alreadyOnLogin) { 45 const next = encodeURIComponent(current || '/'); 46 window.location.href = `${loginPath}`; 47 return; 48 } 49 } 50 return Promise.reject(error); 51 }, 52); 53 54function buildFormData(formData, data, parentKey = '') { 55 if (data && typeof data === 'object' && !(data instanceof File)) { 56 Object.keys(data).forEach((key) => { 57 const value = data[key]; 58 const fullKey = parentKey ? `${parentKey}[${key}]` : key; 59 buildFormData(formData, value, fullKey); 60 }); 61 } else if (data !== null && data !== undefined) { 62 formData.append(parentKey, data); 63 } 64} 65 66window.api.serverRequest = async function ( 67 url, 68 data = null, 69 method = 'GET', 70 accept = '', 71 redirect = true, 72) { 73 try { 74 // СбоÑка Ñела 75 let payload; 76 if (data instanceof FormData) { 77 payload = data; 78 } else if (data) { 79 payload = JSON.stringify(data); 80 } 81 82 // Ðаголовки 83 const headers = {}; 84 if (accept) { 85 if (accept.RECAPTCHA_HEADER_TOKEN) { 86 headers['RECAPTCHA-HEADER-TOKEN'] = accept.RECAPTCHA_HEADER_TOKEN; 87 } else { 88 headers['accept'] = accept; 89 } 90 } 91 // Hub-Payload-Token 92 const timestamp = Date.now(); 93 headers['Hub-Payload-Token'] = `${timestamp}${md5(timestamp)}W`; 94 95 // ÐÑполнÑем запÑÐ¾Ñ 96 const response = await window.api.request({ 97 url, 98 method, 99 data: payload, 100 headers, 101 }); 102 103 // Redirect-логика 104 if (response.data && response.data.redirect_url) { 105 if (redirect) { 106 window.location = response.data.redirect_url; 107 } else { 108 return response.data; 109 } 110 } 111 112 return response.data; 113 } catch (error) { 114 const status = error.response?.status; 115 if (status === 401) { 116 MicroModal.close(); 117 MicroModal.show('AuthModal'); 118 } 119 if (status === 500) { 120 toastMessage('ÐÑибка на ÑеÑвеÑе', 'error'); 121 } 122 const respJSON = error.response?.data; 123 if (respJSON) { 124 if (respJSON.non_field_errors) { 125 toastMessage(respJSON.non_field_errors[0], 'error'); 126 } 127 return respJSON; 128 } 129 throw error; 130 } 131}; 132 133window.toast = function (message, options = {}) { 134 let description = ''; 135 let type = 'success'; 136 let position = 'top-center'; 137 let html = ''; 138 if (typeof options.description != 'undefined') description = options.description; 139 if (typeof options.type != 'undefined') type = options.type; 140 if (typeof options.position != 'undefined') position = options.position; 141 if (typeof options.html != 'undefined') html = options.html; 142 143 window.dispatchEvent( 144 new CustomEvent('toast-show', { 145 detail: { 146 type: type, 147 message: message, 148 description: description, 149 position: position, 150 html: html, 151 }, 152 }), 153 ); 154}; 155 156window.goToReturnUrl = function () { 157 const target = new URLSearchParams(window.location.search).get('return'); 158 if (!target) return false; 159 let url; 160 try { 161 url = new URL(target, window.location.origin); 162 } catch (_) { 163 return false;
164 } 165 if (url.origin !== window.location.origin) return false; 166 window.location.href = url.href; 167 return true; 168}; 169 170window.uploadErrorFallback = function () { 171 return (window.UPLOAD_ERROR_FALLBACK || '').trim(); 172}; 173 174window.uploadTooLargeMessage = function () { 175 return (window.UPLOAD_TOO_LARGE_MESSAGE || '').trim(); 176}; 177 178window.uploadErrorMessage = function (error, fallback) { 179 const data = (error && error.response && error.response.data) || (error && error.data) || error; 180 if (data instanceof Error) { 181 return fallback || window.uploadErrorFallback(); 182 } 183 if (typeof data === 'string' && data.trim() && data.indexOf('<') !== 0) { 184 return data.trim(); 185 } 186 if (data && typeof data === 'object') { 187 if (typeof data.detail === 'string' && data.detail.trim()) { 188 return data.detail; 189 } 190 for (const key of ['non_field_errors', 'file', 'image', 'message']) { 191 const value = data[key]; 192 if (Array.isArray(value) && value.length && typeof value[0] === 'string') { 193 return value[0]; 194 } 195 if (typeof value === 'string' && value.trim()) { 196 return value; 197 } 198 } 199 } 200 return fallback || window.uploadErrorFallback(); 201}; 202 203window.showUploadError = function (error, fallback) { 204 console.error('Upload failed:', error); 205 const message = window.uploadErrorMessage(error, fallback); 206 if (message && typeof window.toast === 'function') { 207 window.toast(message, { type: 'danger' }); 208 } 209 return message; 210}; 211 212document.addEventListener('alpine:init', () => { 213 Alpine.store('modals', { 214 modals: {}, 215 activeModal: null, 216 stack: [], 217 sizes: { 218 xs: 'max-w-xs', 219 sm: 'max-w-sm', 220 md: 'max-w-[400px]', 221 lg: 'max-w-lg', 222 xl: 'max-w-[760px]', 223 '2xl': 'max-w-[760px]', 224 '3xl': 'max-w-3xl', 225 '4xl': 'max-w-4xl', 226 '5xl': 'max-w-5xl', 227 '6xl': 'max-w-6xl', 228 '7xl': 'max-w-7xl', 229 full: 'max-w-full', 230 }, 231 init() {}, 232 register(name, size = 'lg') { 233 if (!this.modals[name]) { 234 this.modals[name] = { 235 open: false, 236 header: '', 237 body: '', 238 footer: '', 239 size: this.getSizeClass(size), 240 customSize: null, 241 persistent: false, 242 }; 243 } 244 return this.modals[name]; 245 }, 246 hasContent(name) { 247 const modal = this.modals[name]; 248 return !!(modal && (modal.body || modal.header || modal.footer)); 249 }, 250 getSizeClass(size) { 251 // Return custom size if provided as direct class 252 if (size.startsWith('max-w-') || size.startsWith('w-')) { 253 return size; 254 } 255 // Return predefined size 256 return this.sizes[size] || this.sizes['lg']; 257 }, 258 open(name, options = {}) { 259 const modal = this.register(name, options.size); 260 this.stack = this.stack.filter((n) => n !== name); 261 this.stack.push(name); 262 this.activeModal = name; 263 this._syncScrollLock(); 264 265 // Update modal properties 266 modal.open = true; 267 268 // Persistent modals can only be closed programmatically (not via Esc / overlay click). 269 modal.persistent = !!options.persistent; 270 271 if (options.header) modal.header = options.header; 272 if (options.body) modal.body = options.body; 273 if (options.footer) modal.footer = options.footer; 274 if (options.size) modal.size = this.getSizeClass(options.size); 275 if (options.id) modal.id = options.id; 276 277 // For completely custom sizes 278 if (options.customSize) { 279 modal.size = options.customSize; 280 } 281 }, 282 close(name = null) { 283 const target = name || this.activeModal; 284 if (target && this.modals[target]) { 285 this.modals[target].open = false; 286 this.stack = this.stack.filter((n) => n !== target); 287 this.activeModal = this.stack[this.stack.length - 1] || null; 288 this._syncScrollLock(); 289 } 290 }, 291 292 _scrollLocked: false, 293 _prevOverflow: '', 294 _prevPaddingRight: '', 295 _syncScrollLock() { 296 const anyOpen = this.stack.length > 0; 297 const root = document.documentElement; 298 if (anyOpen && !this._scrollLocked) { 299 this._scrollLocked = true; 300 this._prevOverflow = root.style.overflow; 301 this._prevPaddingRight = root.style.paddingRight; 302 const scrollbarWidth = window.innerWidth - root.clientWidth; 303 root.style.overflow = 'hidden'; 304 const gutter = window.getComputedStyle(root).scrollbarGutter; 305 if (!gutter || gutter === 'auto') { 306 root.style.paddingRight = `${scrollbarWidth}px`; 307 } 308 } else if (!anyOpen && this._scrollLocked) { 309 this._scrollLocked = false;
310 root.style.overflow = this._prevOverflow; 311 root.style.paddingRight = this._prevPaddingRight; 312 } 313 }, 314 315 isOpen(name) { 316 return this.modals[name]?.open || false; 317 }, 318 }); 319 320 // Register the modal directive 321 Alpine.directive('modal', (el, { value }, { Alpine }) => { 322 const modalName = value; 323 const store = Alpine.store('modals'); 324 325 // Register the modal 326 store.register(modalName); 327 328 // Extract content from slots 329 const header = el.querySelector('modal-header')?.innerHTML || ''; 330 const body = el.querySelector('modal-body')?.innerHTML || ''; 331 const footer = el.querySelector('modal-footer')?.innerHTML || ''; 332 333 // Update modal content 334 store.modals[modalName].header = header; 335 store.modals[modalName].body = body; 336 store.modals[modalName].footer = footer; 337 338 // Remove the original element from DOM 339 el.remove(); 340 }); 341 342 Alpine.directive('dropdown', (el) => { 343 if (!el.hasAttribute('x-dropdown')) return; 344 let state = Alpine.reactive({ open: false }); 345 346 if (!el._dropdownInit) { 347 el._dropdownInit = true; 348 349 const trigger = el.querySelector('[x-dropdown\\:trigger]'); 350 const menu = el.querySelector('[x-dropdown\\:menu]'); 351 352 // === Apply dropdown container classes === 353 el.classList.add('relative', 'inline-block', 'text-left'); 354 355 if (trigger && menu) { 356 // Ensure Alpine context on menu 357 if (!menu._x_dataStack) Alpine.initTree(menu); 358 359 // Alpine.effect(() => { 360 // if (state.open) { 361 // const triggerWidth = trigger.offsetWidth; 362 // menu.style.minWidth = `${triggerWidth}px`; 363 // } 364 // }); 365 366 menu.classList.add( 367 'ah-dropdown-menu', 368 'absolute', 369 'right-0', 370 'mx-2', 371 'max-h-80', 372 'overflow-auto', 373 'origin-top-right', 374 'rounded-sm', 375 'bg-white', 376 'ring-1', 377 'ring-black', 378 'ring-opacity-5', 379 'z-50', 380 'transform', 381 'transition', 382 'duration-200', 383 'ease-out', 384 'opacity-0', 385 '-translate-y-2', 386 'pointer-events-none', 387 'p-1', 388 'space-y-1', 389 'bg-surface-primary-background', 390 ); 391 392 Alpine.effect(() => { 393 if (state.open) { 394 menu.style.display = 'block'; 395 requestAnimationFrame(() => { 396 menu.classList.remove('opacity-0', '-translate-x-2', 'pointer-events-none'); 397 menu.classList.add('opacity-100', 'translate-x-0', 'pointer-events-auto'); 398 }); 399 } else { 400 menu.classList.remove('opacity-100', 'translate-x-0', 'pointer-events-auto'); 401 menu.classList.add('opacity-0', '-translate-x-2', 'pointer-events-none'); 402 setTimeout(() => { 403 menu.style.display = 'none'; 404 }, 200); 405 } 406 }); 407 408 trigger.addEventListener('click', () => { 409 state.open = !state.open; 410 }); 411 412 menu.addEventListener('click', () => { 413 state.open = !state.open; 414 }); 415 416 document.addEventListener('click', (e) => { 417 if (!el.contains(e.target)) state.open = false; 418 }); 419 } 420 } 421 }); 422}); 423 424window.COMMUNITY_ROLE_VALUES = ['tech_community', 'innovator']; 425window.isCommunityRole = (value) => window.COMMUNITY_ROLE_VALUES.includes(value); 426 427function multiSelect({ 428 name = 'selected', 429 items = [], 430 selected = [], 431 limit = null, 432 onChange = null, 433 allowCustom = false, 434 minCustomLength = 0, 435}) { 436 return { 437 name, 438 open: false, 439 search: '', 440 items, 441 limit, 442 allowCustom, 443 minCustomLength, 444 selectedIds: selected, 445 get selectedItems() { 446 return this.items.filter((i) => this.selectedIds.includes(i.id)); 447 }, 448 get filteredItems() { 449 return this.items.filter((i) => i.value.toLowerCase().includes(this.search.toLowerCase())); 450 }, 451 get canAddCustom() { 452 if (!this.allowCustom) return false; 453 const s = this.search.trim(); 454 if (!s || s.length < this.minCustomLength) return false;
455 const sl = s.toLowerCase(); 456 return !this.items.some((i) => i.value.toLowerCase() === sl); 457 }, 458 get customTooShort() { 459 if (!this.allowCustom || !this.minCustomLength) return false; 460 const len = this.search.trim().length; 461 return len > 0 && len < this.minCustomLength; 462 }, 463 get minLengthHint() { 464 const n = this.minCustomLength; 465 const lang = (typeof LANG !== 'undefined' ? LANG : 'ru') || 'ru'; 466 if (lang === 'en') return `At least ${n} characters`; 467 if (lang === 'kk') return `ÐемÑнде ${n} Ñаңба`; 468 const n1 = n % 10; 469 const n2 = n % 100; 470 let word = 'Ñимволов'; 471 if (n1 === 1 && n2 !== 11) word = 'Ñимвол'; 472 else if (n1 >= 2 && n1 <= 4 && (n2 < 10 || n2 >= 20)) word = 'Ñимвола'; 473 return `ÐинимÑм ${n} ${word}`; 474 }, 475 setItems(arr) { 476 this.items = arr || []; 477 }, 478 isSelected(item) { 479 return this.selectedIds.includes(item.id); 480 }, 481 isDisabled(item) { 482 // disable if limit reached and item not selected yet 483 return this.limit !== null && this.selectedIds.length >= this.limit && !this.isSelected(item); 484 }, 485 addCustom() { 486 const val = this.search.trim(); 487 if (!val || val.length < this.minCustomLength) return; 488 const existing = this.items.find((i) => i.value.toLowerCase() === val.toLowerCase()); 489 if (existing) { 490 if (!this.isSelected(existing)) this.toggle(existing); 491 } else { 492 const newItem = { id: val, value: val }; 493 this.items.push(newItem); 494 this.selectedIds.push(val); 495 if (typeof onChange === 'function') { 496 onChange([...this.selectedIds]); 497 } 498 } 499 this.search = ''; 500 }, 501 toggle(item) { 502 if (this.isDisabled(item)) return; // do nothing if disabled 503 504 const index = this.selectedIds.indexOf(item.id); 505 if (index > -1) { 506 this.selectedIds.splice(index, 1); 507 } else { 508 this.selectedIds.push(item.id); 509 } 510 511 if (typeof onChange === 'function') { 512 onChange([...this.selectedIds]); 513 } 514 }, 515 }; 516} 517 518function singleSelect({ items = [], value = '', disabled = false, onChange = null }) { 519 const resolve = (source) => (typeof source === 'function' ? source() : source); 520 return { 521 open: false, 522 search: '', 523 get items() { 524 return (resolve(items) || []).filter((item) => item && item.id); 525 }, 526 get value() { 527 return resolve(value) || ''; 528 }, 529 get isDisabled() { 530 return !!resolve(disabled); 531 }, 532 get selectedItem() { 533 if (!this.value) return null; 534 return this.items.find((item) => item.id === this.value) || null; 535 }, 536 get label() { 537 return this.selectedItem ? this.selectedItem.value : this.value; 538 }, 539 get filteredItems() { 540 const query = this.search.trim().toLowerCase(); 541 if (!query) return this.items; 542 return this.items.filter((item) => (item.value || '').toLowerCase().includes(query)); 543 }, 544 isSelected(item) { 545 return !!this.value && item.id === this.value; 546 }, 547 toggleOpen() { 548 if (this.isDisabled) return; 549 this.open = !this.open; 550 if (this.open) { 551 this.search = ''; 552 this.$nextTick(() => { 553 const panel = this.$el.querySelector('.overflow-y-auto'); 554 if (panel) panel.scrollTop = 0; 555 }); 556 } 557 }, 558 select(item) { 559 this.open = false; 560 this.search = ''; 561 if (typeof onChange === 'function') { 562 onChange(item.id); 563 } 564 }, 565 }; 566} 567 568(() => { 569 const normISO = (s) => 570 String(s) 571 .trim() 572 .replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)(.*)$/, '$1T$2$3') 573 .replace(/(\.\d{3})\d+(?=(?:Z|[+-]\d{2}:?\d{2})?$)/, '$1') 574 .replace(/([+-]\d{2})(\d{2})$/, '$1:$2'); 575 576 const parseISO = (s) => { 577 const d = new Date(normISO(s)); 578 return isNaN(d) ? null : d; 579 }; 580 581 const declRu = (n, [one, few, many]) => { 582 n = Math.abs(n) % 100; 583 const n1 = n % 10; 584 if (n > 10 && n < 20) return many; 585 if (n1 > 1 && n1 < 5) return few; 586 if (n1 === 1) return one; 587 return many; 588 }; 589 590 const I18N = { 591 ru: { 592 locale: 'ru-RU', 593 suffix: 'назад', 594 words: { 595 minute: ['минÑÑа', 'минÑÑÑ', 'минÑÑ'], 596 hour: ['ÑаÑ', 'ÑаÑа', 'ÑаÑов'], 597 day: ['денÑ', 'днÑ', 'дней'], 598 }, 599 decl: declRu, 600 }, 601 kk: { 602 locale: 'kk-KZ', 603 suffix: 'бұÑÑн', 604 words: { 605 minute: 'минÑÑ', 606 hour: 'ÑаÒаÑ', 607 day: 'күн', 608 }, 609 months: ['Òаң', 'ÐÒп', 'ÐаÑ', 'СÓÑ', 'Ðам', 'ÐаÑ', 'ШÑл', 'Там', 'ÒÑÑ', 'Òаз', 'ÒаÑ', 'Ðел'], 610 decl: (n, w) => (typeof w === 'string' ? w : n === 1 ? w[0] : w[1] || w[0]), 611 }, 612 en: { 613 locale: 'en-GB', 614 suffix: 'ago', 615 words: { 616 minute: ['minute', 'minutes'], 617 hour: ['hour', 'hours'], 618 day: ['day', 'days'], 619 }, 620 decl: (n, [one, many]) => (n === 1 ? one : many), 621 }, 622 }; 623 624 const pickLang = (fallback = 'ru') => { 625 const raw = (LANG || '').toString().trim().toLowerCase(); 626 if (raw === 'ru' || raw === 'kk' || raw === 'en') return raw; 627 return fallback; 628 }; 629 630 function formatAbs(d, locale = 'ru', timeZone = 'Asia/Almaty') { 631 const i18nEntry = I18N[locale] || I18N.ru; 632 const loc = i18nEntry.locale || I18N.ru.locale; 633 const parts = new Intl.DateTimeFormat(loc, { 634 timeZone, 635 year: 'numeric', 636 month: 'short', 637 day: '2-digit', 638 hour: '2-digit', 639 minute: '2-digit', 640 hour12: false, 641 }).formatToParts(d); 642 const get = (t) => parts.find((p) => p.type === t)?.value || ''; 643 let mon; 644 if (i18nEntry.months) { 645 const monthNum = parseInt(new Intl.DateTimeFormat('en', { timeZone, month: 'numeric' }).format(d), 10); 646 const monthName = i18nEntry.months[monthNum - 1]; 647 mon = monthName ? monthName + '.' : ''; 648 } else { 649 mon = get('month'); 650 mon = mon ? mon[0].toUpperCase() + mon.slice(1) : ''; 651 if (mon && !mon.endsWith('.')) mon += '.'; 652 } 653 return `${mon} ${get('day')}, ${get('year')} ${get('hour')}:${get('minute')}`; 654 } 655 656 function displayDuration(iso, opts = {}) { 657 const { 658 locale = pickLang('ru'), 659 timeZone = 'Asia/Almaty', 660 now = new Date(), 661 } = opts; 662 663 const lang = ['ru', 'kk', 'en'].includes((locale || '').toLowerCase()) 664 ? locale.toLowerCase() 665 : pickLang('ru'); 666 const i18n = I18N[lang] || I18N.ru; 667 668 const d = iso instanceof Date ? iso : parseISO(iso); 669 if (!d) return ''; 670 671 let diffSec = Math.floor((now.getTime() - d.getTime()) / 1000); 672 if (diffSec < 0) return formatAbs(d, lang, timeZone); 673 674 const minutes = Math.floor(diffSec / 60); 675 const hours = Math.floor(minutes / 60); 676 const days = Math.floor(hours / 24); 677 678 if (hours < 1) { 679 const n = Math.max(1, minutes); 680 const w = i18n.words.minute; 681 const word = Array.isArray(w) ? i18n.decl(n, w) : w; 682 return `${n} ${word} ${i18n.suffix}`; 683 } 684 685 if (days < 1) { 686 const n = Math.max(1, hours); 687 const w = i18n.words.hour; 688 const word = Array.isArray(w) ? i18n.decl(n, w) : w; 689 return `${n} ${word} ${i18n.suffix}`; 690 } 691 692 if (days < 2) { 693 const n = Math.max(1, days); 694 const w = i18n.words.day; 695 const word = Array.isArray(w) ? i18n.decl(n, w) : w; 696 return `${n} ${word} ${i18n.suffix}`; 697 } 698 699 return formatAbs(d, lang, timeZone); 700 } 701
702 window.displayDuration = displayDuration; 703})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.