PageSourceSearch

https://antishkola.com/js/change-request.js?v=1

js antishkola.com collected 2026-10-09 23:06:08 UTC 13,096 bytes, 313 lines download raw bytes

1/*
2 * change-request.js — встроенный виджет «Предложить изменение» для сайта antishkola.com
3 *
4 * Интегрирован с базой данных и системой change_requests Марии (src/maria/change_requests.py).
5 * Загружается ТОЛЬКО лениво по хоткею (Cmd+Shift+B / Ctrl+Shift+B) или тройному тапу.
6 * Не влияет на скорость сайта, SEO и Core Web Vitals обычных посетителей.
7 */
8(function () {
9  'use strict';
10
11  if (window.__antishkolaCRObject) return;
12
13  var recentErrors = [];
14  var origError = window.console && window.console.error;
15  if (origError) {
16    window.console.error = function () {
17      try {
18        var msg = Array.prototype.slice.call(arguments).map(String).join(' ');
19        recentErrors.push(msg.slice(0, 300));
20        if (recentErrors.length > 5) recentErrors.shift();
21      } catch (_) {}
22      origError.apply(window.console, arguments);
23    };
24  }
25
26  var STYLES = '' +
27    '#as-cr-overlay {' +
28    '  position: fixed; inset: 0; z-index: 2147483647; background: rgba(10,15,18,0.78);' +
29    '  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);' +
30    '  display: flex; align-items: center; justify-content: center; padding: 16px;' +
31    '  font-family: -apple-system, BlinkMacSystemFont, "Manrope", "Segoe UI", Roboto, sans-serif;' +
32    '  opacity: 0; pointer-events: none; transition: opacity 0.2s cubic-bezier(0.4,0,0.2,1);' +
33    '}' +
34    '#as-cr-overlay.is-visible { opacity: 1; pointer-events: auto; }' +
35    '#as-cr-modal {' +
36    '  width: 100%; max-width: 520px; background: #0A0F12; color: #fff;' +
37    '  border: 1px solid rgba(255,255,255,0.14); border-radius: 20px;' +
38    '  box-shadow: 0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(217,22,176,0.2);' +
39    '  overflow: hidden; display: flex; flex-direction: column;' +
40    '  transform: scale(0.96) translateY(10px); transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);' +
41    '}' +
42    '#as-cr-overlay.is-visible #as-cr-modal { transform: scale(1) translateY(0); }' +
43    '#as-cr-header {' +
44    '  padding: 18px 22px 14px; border-bottom: 1px solid rgba(255,255,255,0.08);' +
45    '  display: flex; align-items: center; justify-content: space-between;' +
46    '}' +
47    '#as-cr-header h3 {' +
48    '  margin: 0; font-size: 17px; font-weight: 700; color: #fff;' +
49    '  display: flex; align-items: center; gap: 8px;' +
50    '}' +
51    '#as-cr-header h3 span { color: #d916b0; font-size: 18px; }' +
52    '#as-cr-close {' +
53    '  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12);' +
54    '  color: #fff; width: 32px; height: 32px; border-radius: 8px; cursor: pointer;' +
55    '  display: flex; align-items: center; justify-content: center; font-size: 16px;' +
56    '  transition: 0.15s;' +
57    '}' +
58    '#as-cr-close:hover { background: rgba(255,255,255,0.18); }' +
59    '#as-cr-body { padding: 18px 22px 22px; display: flex; flex-direction: column; gap: 14px; }' +
60    '.as-cr-row { display: flex; flex-direction: column; gap: 6px; }' +
61    '.as-cr-row label { font-size: 12.5px; font-weight: 600; color: rgba(255,255,255,0.7); }' +
62    '.as-cr-input, .as-cr-textarea {' +
63    '  width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.14);' +
64    '  border-radius: 10px; padding: 11px 14px; font-size: 14.5px; color: #fff;' +
65    '  font-family: inherit; box-sizing: border-box; outline: none; transition: 0.2s;' +
66    '}' +
67    '.as-cr-input:focus, .as-cr-textarea:focus {' +
68    '  border-color: #d916b0; background: rgba(255,255,255,0.08);' +
69    '  box-shadow: 0 0 0 3px rgba(217,22,176,0.2);' +
70    '}' +
71    '.as-cr-textarea { resize: vertical; min-height: 95px; line-height: 1.5; }' +
72    '#as-cr-context {' +
73    '  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);' +
74    '  border-radius: 10px; padding: 10px 12px; font-size: 11.5px; color: rgba(255,255,255,0.5);' +
75    '  font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;' +
76    '  line-height: 1.45; word-break: break-all;' +
77    '}
77' +
78    '#as-cr-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px; }' +
79    '.as-cr-btn {' +
80    '  padding: 11px 20px; border-radius: 10px; font-size: 14px; font-weight: 700;' +
81    '  cursor: pointer; border: none; transition: 0.15s; font-family: inherit;' +
82    '}' +
83    '.as-cr-btn-cancel {' +
84    '  background: transparent; color: rgba(255,255,255,0.6);' +
85    '  border: 1px solid rgba(255,255,255,0.12);' +
86    '}' +
87    '.as-cr-btn-cancel:hover { background: rgba(255,255,255,0.06); color: #fff; }' +
88    '.as-cr-btn-submit {' +
89    '  background: #d916b0; color: #fff; box-shadow: 0 4px 14px rgba(217,22,176,0.35);' +
90    '}' +
91    '.as-cr-btn-submit:hover { background: #b3128f; }' +
92    '.as-cr-btn-submit:disabled { opacity: 0.5; cursor: not-allowed; }' +
93    '#as-cr-success {' +
94    '  padding: 36px 20px; text-align: center; display: none;' +
95    '}' +
96    '#as-cr-success h4 { font-size: 18px; margin: 0 0 8px; color: #51cf66; }' +
97    '#as-cr-success p { font-size: 13.5px; color: rgba(255,255,255,0.7); margin: 0; }';
98
99  function injectDOM() {
100    var style = document.createElement('style');
101    style.id = 'as-cr-styles';
102    style.textContent = STYLES;
103    document.head.appendChild(style);
104
105    var wrap = document.createElement('div');
106    wrap.id = 'as-cr-overlay';
107    wrap.setAttribute('aria-hidden', 'true');
108    wrap.innerHTML = '' +
109      '<div id="as-cr-modal" role="dialog" aria-modal="true" aria-labelledby="as-cr-title">' +
110      '  <div id="as-cr-header">' +
111      '    <h3 id="as-cr-title"><span>💡</span> Предложить изменение</h3>' +
112      '    <button type="button" id="as-cr-close" aria-label="Закрыть">✕</button>' +
113      '  </div>' +
114      '  <form id="as-cr-form">' +
115      '    <div id="as-cr-body">' +
116      '      <div class="as-cr-row">' +
117      '        <label for="as-cr-author">Автор:</label>' +
118      '        <input type="text" id="as-cr-author" class="as-cr-input" placeholder="Ваше имя" required autocomplete="name">' +
119      '      </div>' +
120      '      <div class="as-cr-row">' +
121      '        <label for="as-cr-text">Что и где изменить:</label>' +
122      '        <textarea id="as-cr-text" class="as-cr-textarea" placeholder="Что и где изменить..." required></textarea>' +
123      '      </div>' +
124      '      <div id="as-cr-context"></div>' +
125      '      <div id="as-cr-actions">' +
126      '        <button type="button" class="as-cr-btn as-cr-btn-cancel" id="as-cr-cancel-btn">Отмена</button>' +
127      '        <button type="submit" class="as-cr-btn as-cr-btn-submit" id="as-cr-submit-btn">Отправить в базу</button>' +
128      '      </div>' +
129      '    </div>' +
130      '  </form>' +
131      '  <div id="as-cr-success">' +
132      '    <h4>✅ Предложение отправлено!</h4>' +
133      '    <p>Запись создана в базе Maria CRM. Спасибо за помощь!</p>' +
134      '  </div>' +
135      '</div>';
136
137    document.body.appendChild(wrap);
138    bindEvents(wrap);
139  }
140
141  function getAutoContext() {
142    var host = window.location.hostname || '';
143    var prefix = 'website:';
144    if (host.indexOf('playsend') !== -1) prefix = 'website:playsend:';
145    else if (host.indexOf('nativespeakers') !== -1) prefix = 'website:ns:';
146    else prefix = 'website:as:';
147
148    var cur = (document.querySelector('#asp-root') && document.querySelector('#asp-root').getAttribute('data-cur')) || 'default';
149    var path = window.location.pathname || '/';
150    var slug = path.replace(/^\/|\/$/g, '').replace(/\//g, '_');
151    var sectionName = prefix + (slug || 'main');
152
153    var tz = 'unknown';
154    try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (_) {}
155
156    return {
157      section: sectionName,
158      page_url: window.location.href,
159      referrer: document.referrer || '',
160      viewport: window.innerWidth + 'x' + window.innerHeight,
161      screen: (window.screen ? (window.screen.width + 'x' + window.screen.height) : 'unknown') + ' @' + (window.devicePixelRatio || 1) + 'x',
162      currency: cur,
163      timezone: tz,
164      language: navigator.language || '',
165      recent_errors: recentErrors.join('; '),
166      user_agent: navigator.userAgent || 'unknown'
167    };
168  }
169
170  function openModal() {
171    var overlay = document.getElementById('as-cr-overlay');
172    if (!overlay) {
173      injectDOM();
174      overlay = document.getElementById('as-cr-overlay');
175    }
176
177    var ctx = getAutoContext();
178    var ctxBox = document.getElementById('as-cr-context');
179    if (ctxBox) {
180      ctxBox.textContent = '📍 ' + ctx.page_url + '\n📱 Экран: ' + ctx.viewport + ' | Валюта: ' + ctx.currency;
181    }
182
183    var authorInput = document.getElementById('as-cr-author');
184    if (authorInput) {
185      var saved = localStorage.getItem('antishkola_operator_name');
186      if (saved) authorInput.value = saved;
187    }
188
189    var form = document.getElementById('as-cr-form');
190    var succ = document.getElementById('as-cr-success');
191    if (form) form.style.display = 'block';
192    if (succ) succ.style.display = 'none';
193
194    overlay.classList.add('is-visible');
195    overlay.setAttribute('aria-hidden', 'false');
196
197    var textInput = document.getElementById('as-cr-text');
198    setTimeout(function () {
199      if (authorInput && !authorInput.value) {
200        authorInput.focus();
201      } else if (textInput) {
202        textInput.focus();
203      }
204    }, 150);
205  }
206
207  function closeModal() {
208    var overlay = document.getElementById('as-cr-overlay');
209    if (!overlay) return;
210    overlay.classList.remove('is-visible');
211    overlay.setAttribute('aria-hidden', 'true');
212    var textInput = document.getElementById('as-cr-text');
213    if (textInput) textInput.value = '';
214  }
215
216  function bindEvents(overlay) {
217    var closeBtn = document.getElementById('as-cr-close');
218    var cancelBtn = document.getElementById('as-cr-cancel-btn');
219    var form = document.getElementById('as-cr-form');
220
221    if (closeBtn) closeBtn.addEventListener('click', closeModal);
222    if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
223
224    overlay.addEventListener('click', function (e) {
225      if (e.target === overlay) closeModal();
226    });
227
228    document.addEventListener('keydown', function (e) {
229      if (e.key === 'Escape' && overlay.classList.contains('is-visible')) {
230        closeModal();
231      }
232    });
233
234    if (form) {
235      form.addEventListener('submit', function (e) {
236        e.preventDefault();
237        var author = (document.getElementById('as-cr-author').value || '').trim();
238        var text = (document.getElementById('as-cr-text').value || '').trim();
239        var submitBtn = document.getElementById('as-cr-submit-btn');
240
241        if (!text) return;
242        if (author) localStorage.setItem('antishkola_operator_name', author);
243
244        if (submitBtn) {
245          submitBtn.disabled = true;
246          submitBtn.textContent = 'Отправка...';
247        }
248
249        var ctx = getAutoContext();
250        var payload = {
251          body: text,
252          author: author || 'operator',
253          section: ctx.section,
254          page_url: ctx.page_url,
255          context: {
256            viewport: ctx.viewport,
257            screen: ctx.screen,
258            currency: ctx.currency,
259            user_agent: ctx.user_agent,
260            recent_errors: ctx.recent_errors,
261            source: 'website_hotkey'
262          }
263        };
264
265        fetch('https://sec.antishkola.com/change-request', {
266          method: 'POST',
267          headers: { 'Content-Type': 'application/json' },
268          body: JSON.stringify(payload)
269        }).then(function (res) {
270          if (!res.ok) {
271            throw new Error('HTTP ' + res.status);
272          }
273          return res.json();
274        }).then(function () {
275          showSuccess();
276        }).catch(function (err) {
277          showError(err);
278        });
279      });
280    }
281  }
282
283  function showError(err) {
284    var submitBtn = document.getElementById('as-cr-submit-btn');
285    if (submitBtn) {
286      submitBtn.disabled = false;
287      submitBtn.textContent = '❌ Ошибка сети. Повторить?';
288      submitBtn.style.background = '#e03131';
289      setTimeout(function () {
290        if (submitBtn) {
291          submitBtn.textContent = 'Отправить в базу';
292          submitBtn.style.background = '#d916b0';
293        }
294      }, 4000);
295    }
296  }
297
298  function showSuccess() {
299    var form = document.getElementById('as-cr-form');
300    var succ = document.getElementById('as-cr-success');
301    var submitBtn = document.getElementById('as-cr-submit-btn');
302    if (form) form.style.display = 'none';
303    if (succ) succ.style.display = 'block';
304    if (submitBtn) {
305      submitBtn.disabled = false;
306      submitBtn.textContent = 'Отправить в базу';
307    }
308    setTimeout(closeModal, 2000);
309  }
310
311  window.__antishkolaChangeRequestOpen = openModal;
312  window.__antishkolaCRObject = true;
313})();

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.