PageSourceSearch

https://cdn.astanahub.com/static/js_v2/axios.js?v=prod-30.09.2026-1

js astanahub.com collected 2026-10-01 10:39:20 UTC 20,515 bytes, 703 lines download raw bytes

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.