PageSourceSearch

https://repair-dreame-msk.ru/assets/app.js?v=20260730phonefixdedupe20260730-115002&privacy_rev=20260914b

js repair-dreame-msk.ru collected 2026-10-10 00:16:21 UTC 13,255 bytes, 322 lines download raw bytes

1/* PRIVACY PATCH 20260914 */
2(() => {
3  const phoneHref = "tel:+74950218385";
4  const modal = document.getElementById("requestModal");
5  const form = modal ? modal.querySelector(".request-form") : null;
6  const isMobile = () => window.matchMedia("(max-width: 760px)").matches;
7  const storageKey = "dreame_traffic_source";
8  const sessionKey = "seo_direct_session_id";
9
10  const getSessionId = () => {
11    let sessionId = localStorage.getItem(sessionKey);
12    if (!sessionId) {
13      sessionId = window.crypto && typeof window.crypto.randomUUID === "function"
14        ? window.crypto.randomUUID()
15        : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
16      localStorage.setItem(sessionKey, sessionId);
17    }
18    return sessionId;
19  };
20
21  const isInternalReferrer = (referrer) => {
22    if (!referrer) return false;
23    try {
24      return new URL(referrer).hostname.replace(/^www\./, "") === location.hostname.replace(/^www\./, "");
25    } catch (error) {
26      return false;
27    }
28  };
29
30  const getTrafficData = () => {
31    const params = new URLSearchParams(location.search);
32    const referrer = document.referrer || "";
33    const utmSource = params.get("utm_source") || "";
34    const utmMedium = params.get("utm_medium") || "";
35    let trafficSource = "";
36
37    if (params.has("yclid") || (/yandex/i.test(utmSource) && /cpc|ppc|paid|direct/i.test(utmMedium))) {
38      trafficSource = "Яндекс Директ";
39    } else if (params.has("gclid") || params.has("gbraid") || params.has("wbraid") || (/google/i.test(utmSource) && /cpc|ppc|paid/i.test(utmMedium))) {
40      trafficSource = "Google Ads";
41    } else if (/yandex\./i.test(referrer)) {
42      trafficSource = "Яндекс SEO";
43    } else if (/google\./i.test(referrer)) {
44      trafficSource = "Google SEO";
45    } else if (utmSource) {
46      trafficSource = [utmSource, utmMedium].filter(Boolean).join(" / ");
47    } else if (referrer) {
48      trafficSource = "Переход с сайта";
49    } else {
50      trafficSource = "Прямой заход";
51    }
52
53    const data = {
54      traffic_source: trafficSource,
55      utm_source: utmSource,
56      utm_medium: utmMedium,
57      utm_campaign: params.get("utm_campaign") || "",
58      utm_term: params.get("utm_term") || "",
59      utm_content: params.get("utm_content") || "",
60      referrer,
61      yclid: params.get("yclid") || "",
62      gclid: params.get("gclid") || "",
63    };
64    const saved = JSON.parse(localStorage.getItem(storageKey) || "null");
65    const hasCampaign = params.toString();
66    const hasExternalReferrer = referrer && !isInternalReferrer(referrer);
67    if (!saved || hasCampaign || hasExternalReferrer) localStorage.setItem(storageKey, JSON.stringify(data));
68    return saved && !hasCampaign && !hasExternalReferrer ? saved : data;
69  };
70
71  const fillTrafficFields = () => {
72    if (!form) return;
73    const data = getTrafficData();
74    Object.entries(data).forEach(([key, value]) => {
75      const input = form.elements[key];
76      if (input) input.value = value || "";
77    });
78  };
79
80  const refreshCaptcha = () => {
81    if (!form) return;
82    const a = Math.floor(Math.random() * 8) + 2;
83    const b = Math.floor(Math.random() * 8) + 2;
84    const question = form.querySelector("[data-captcha-question]");
85    if (question) question.textContent = "Проверка: " + a + " + " + b + " =";
86    if (form.elements.captcha_a) form.elements.captcha_a.value = String(a);
87    if (form.elements.captcha_b) form.elements.captcha_b.value = String(b);
88    if (form.elements.captcha) form.elements.captcha.value = "";
89  };
90
91  const openPopup = (button) => {
92    if (!modal || !form) return;
93    fillTrafficFields();
94    refreshCaptcha();
95    form.page.value = location.href;
96    form.source.value = button?.dataset.source || "Кнопка";
97    form.model.value = button?.dataset.model || "";
98    form.device_type.value = button?.dataset.deviceType || "";
99    modal.classList.add("is-open");
100    modal.setAttribute("aria-hidden", "false");
101    const input = form.querySelector("input[name='phone']");
102    if (input) setTimeout(() => input.focus(), 50);
103  };
104
105  const closePopup = () => {
106    if (!modal) return;
107    modal.classList.remove("is-open");
108    modal.setAttribute("aria-hidden", "true");
109  };
110
111  const sendAnalyticsPhoneClick = (data) => {
112    const payload = JSON.stringify({
113      site: location.hostname.replace(/^www\./, ""),
114      session: getSessionId(),
115      source: data.traffic_source || "",
116      event: "phone_click",
117      page: location.pathname + location.search,
118      url: location.href,
119      data,
120    });
121    const endpoint = "https://tracker.xiaomi-remont.ru/api/event";
122    window.__seoDirectSkipNextPhoneClick = true;
123
124    if (navigator.sendBeacon) {
125      const blob = new Blob([payload], { type: "application/json" });
126      if (navigator.sendBeacon(endpoint, blob)) return;
127    }
128    fetch(endpoint, {
129      method: "POST",
130      headers: { "Content-Type": "application/json" },
131      body: payload,
132      keepalive: true,
133      credentials: "omit",
134    }).catch(() => {});
135  };
136
137  const trackPhoneClick = (link, fallbackName = "Ссылка телефона") => {
138    const data = getTrafficData();
139    const payload = new FormData();
140    const href = link?.getAttribute?.("href") || phoneHref;
141    const displayPhone = link?.textContent?.trim() || fallbackName;
142    payload.append("page", location.href);
143    payload.append("session_id", getSessionId());
144    payload.append("display_phone", displayPhone);
145    payload.append("href_phone", href);
146    payload.append("click_place", link?.closest?.(".site-header") ? "Шапка" : link?.closest?.(".mobile-bar") ? "Мобильная нижняя панель" : link?.closest?.(".contacts-page") ? "Контакты" : fallbackName);
147    Object.entries(data).forEach(([key, value]) => payload.append(key, value || ""));
148
149    if (navigator.sendBeacon) {
150      navigator.sendBeacon("/call-track.php", payload);
151    } else {
152      fetch("/call-track.php", { method: "POST", body: payload, keepalive: true }).catch(() => {});
153    }
154
155    sendAnalyticsPhoneClick({
156      ...data,
157      button_name: displayPhone,
158      href,
159      phone: displayPhone,
160      referrer: data.referrer || document.referrer || "",
161      landing_url: location.href,
162    });
163  };
164
165  document.addEventListener("click", (event) => {
166    const phoneLink = event.target.closest("a[href^='tel:']");
167    if (phoneLink) trackPhoneClick(phoneLink);
168
169    const popupButton = event.target.closest(".js-open-popup,.price-action");
170    if (popupButton) {
171      event.preventDefault();
172      if (isMobile() && popupButton.classList.contains("price-action")) {
173        trackPhoneClick(null, popupButton.textContent.trim() || "Кнопка цены");
174        location.href = phoneHref;
175        return;
176      }
177      openPopup(popupButton);
178      return;
179    }
180
181    const mobilePrice = event.target.closest(".table-wrap.active td:nth-child(3)");
182    if (mobilePrice && isMobile()) {
183      event.preventDefault();
184      trackPhoneClick(null, mobilePrice.textContent.trim() || "Строка цены");
185      location.href = phoneHref;
186      return;
187    }
188
189    const modelButton = event.target.closest(".js-model-action");
190    if (modelButton) {
191      event.preventDefault();
192      if (isMobile()) {
193        trackPhoneClick(null, modelButton.textContent.trim() || "Кнопка модели");
194        location.href = phoneHref;
195      } else openPopup(modelButton);
196      return;
197    }
198
199    if (event.target.closest("[data-close-modal]")) closePopup();
200  });
201
202  document.addEventListener("keydown", (event) => {
203    if (event.key === "Escape") closePopup();
204  });
205
206  document.querySelectorAll(".district-shell").forEach((shell) => {
207    const buttons = shell.querySelectorAll("[data-district]");
208    const panels = shell.querySelectorAll("[data-district-panel]");
209    buttons.forEach((button) => {
210      button.addEventListener("click", () => {
211        buttons.forEach((item) => item.classList.toggle("active", item === button));
212        panels.forEach((panel) => panel.classList.toggle("active", panel.dataset.districtPanel === button.dataset.district));
213      });
214    });
215  });
216
217  document.querySelectorAll(".price-section").forEach((section) => {
218    const buttons = section.querySelectorAll("[data-price-tab]");
219    const panels = section.querySelectorAll("[data-price-panel]");
220    const select = section.querySelector("[data-price-select]");
221    const toggle = section.querySelector(".price-select-toggle");
222    const current = section.querySelector(".price-select-current");
223    if (toggle && select) {
224      toggle.addEventListener("click", () => {
225        const open = !select.classList.contains("open");
226        select.classList.toggle("open", open);
227        toggle.setAttribute("aria-expanded", open ? "true" : "false");
228      });
229      document.addEventListener("click", (event) => {
230        if (!select.contains(event.target)) {
231          select.classList.remove("open");
232          toggle.setAttribute("aria-expanded", "false");
233        }
234      });
235    }
236    buttons.forEach((button) => {
237      button.addEventListener("click", () => {
238        buttons.forEach((item) => {
239          const active = item === button;
240          item.classList.toggle("active", active);
241          item.setAttribute("aria-selected", active ? "true" : "false");
242        });
243        panels.forEach((panel) => {
244          panel.classList.toggle("active", panel.dataset.pricePanel === button.dataset.priceTab);
245        });
246        if (current) current.textContent = button.textContent.trim();
247        if (select && toggle) {
248          select.classList.remove("open");
249          toggle.setAttribute("aria-expanded", "false");
250        }
251      });
252    });
253  });
254
255  if (form) {
256    fillTrafficFields();
257    refreshCaptcha();
258    form.addEventListener("submit", async (event) => {
259      event.preventDefault();
260      const status = form.querySelector("[data-form-note]");
261      const captcha = Number(form.elements.captcha?.value || NaN);
262      const expected = Number(form.elements.captcha_a?.value || 0
262) + Number(form.elements.captcha_b?.value || 0);
263      if (!Number.isFinite(captcha) || captcha !== expected) {
264        if (status) status.textContent = "Проверьте ответ на капчу.";
265        refreshCaptcha();
266        return;
267      }
268      fillTrafficFields();
269      if (status) status.textContent = "Отправляем заявку...";
270      try {
271        const response = await fetch(form.action, { method: "POST", body: new FormData(form), headers: { Accept: "application/json" } });
272        const payload = await response.json().catch(() => ({}));
273        if (!response.ok) {
274          if (response.status === 429 && status) {
275            const minutes = Math.max(1, Math.ceil(Number(payload.retry_after || 1800) / 60));
276            status.textContent = `Заявка уже отправлена. Повторная отправка будет доступна примерно через ${minutes} мин.`;
277            return;
278          }
279          throw new Error(payload.error || "send failed");
280        }
281        form.reset();
282        window.location.href = "/spasibo/";
283      } catch (error) {
284        if (status) status.textContent = "Не удалось отправить заявку. Позвоните нам или попробуйте еще раз.";
285      }
286    });
287  }
288
289  const pad = (value) => String(value).padStart(2, "0");
290  const updateCountdown = () => {
291    const now = new Date();
292    const end = new Date(now);
293    end.setHours(23, 59, 59, 999);
294    let diff = Math.max(0, end - now);
295    const hours = Math.floor(diff / 3600000);
296    diff -= hours * 3600000;
297    const minutes = Math.floor(diff / 60000);
298    diff -= minutes * 60000;
299    const seconds = Math.floor(diff / 1000);
300    document.querySelectorAll("[data-countdown]").forEach((node) => {
301      node.textContent = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
302    });
303  };
304  updateCountdown();
305  setInterval(updateCountdown, 1000);
306})();
307;(() => { let started = false; const start = () => { if (started) return; started = true;
308;(() => {
309  if (window.__seoDirectTrackerLoader) return;
310  window.__seoDirectTrackerLoader = true;
311  const script = document.createElement("script");
312  script.src = "https://tracker.xiaomi-remont.ru/tracker.js";
313  script.defer = true;
314  document.head.appendChild(script);
315})();
316}; if (window.sitePrivacyAnalyticsAllowed && window.sitePrivacyAnalyticsAllowed()) start(); else window.addEventListener("privacy:analytics-allowed", start, {once:true}); })();
317
318
319;(() => { let started = false; const start = () => { if (started) return; started = true;
320;(()=>{const i="G-J0XFD6CEQ3";if(document.querySelector("script[data-ga4]"))return;window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){window.dataLayer.push(arguments)};const s=document.createElement("script");s.async=true;s.dataset.ga4=i;s.src="https://www.googletagmanager.com/gtag/js?id="+encodeURIComponent(i);document.head.appendChild(s);window.gtag("js",new Date());window.gtag("config",i)})();
321}; if (window.sitePrivacyAnalyticsAllowed && window.sitePrivacyAnalyticsAllowed()) start(); else window.addEventListener("privacy:analytics-allowed", start, {once:true}); })();
322

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.