PageSourceSearch

https://npz-tactical-map.vercel.app/article-nudge.js?v=a3

js npz-tactical-map.vercel.app collected 2026-10-03 07:24:49 UTC 10,728 bytes, 196 lines download raw bytes

1(function () {
2  "use strict";
3  // Карточка «свежая сводка с обложкой» для карт (idea Серёги 14.07). Карты —
4  // точки входа с высоким отказом (/karta-bpla ~22%): читатель посмотрел и ушёл.
5  // Эта плашка справа-сверху показывает ОБЛОЖКУ последней сводки (на картинке уже
6  // город удара + подпись что произошло) → клик уводит читать /news/<дата>, а не
7  // закрывать вкладку. Растит глубину визита (#5 бэклога роста).
8  // Самодостаточна (инжектит свой CSS), докается в общий window.__nudgeDock из
9  // vpn-nudge.js — одна карточка открыта за раз, не перекрывает VPN/подписку.
10  // ponytail: данные тянет из уже деплоящегося /data/news-archive.json (no-cache),
11  // обложку строит по предсказуемому имени /assets/cover-<дата>.png; при 404
12  // обложки карточка просто не показывается (не ломает страницу).
13
14  var K = "art_nudge";          // localStorage: 'dock' | 'seen:<date>'
15  var GOAL = "art_read";
16  // Версионированная замена заглушки: старое имя закреплено в CDN-кэше.
17  var COVER_OVERRIDES = { "2026-09-11": "/assets/cover-2026-09-11-restored.png" };
18  var MONTHS = ["января","февраля","марта","апреля","мая","июня","июля",
19                "августа","сентября","октября","ноября","декабря"];
20
21  function seenState() { try { return localStorage.getItem(K) || ""; } catch (e) { return ""; } }
22  function markSeen(date) { try { localStorage.setItem(K, "seen:" + date); } catch (e) {} }
23  function track() {
24    try { if (window.ym) ym(110490245, "reachGoal", GOAL); } catch (e) {}
25    try { if (window.va) va("event", { name: GOAL }); } catch (e) {}
26  }
27
28  function rusDate(iso) {
29    var p = String(iso).split("-");
30    if (p.length !== 3) return iso;
31    return parseInt(p[2], 10) + " " + MONTHS[parseInt(p[1], 10) - 1];
32  }
33
34  // Заголовок из strikes сводки: «<город> и ещё N ударов» (стиль страниц /news).
35  function makeTitle(brief) {
36    var s = (brief && brief.strikes) || [];
37    if (!s.length) return "Итоги дня: удары и дефицит топлива";
38    var city = s[0].city || "";
39    var extra = s.length - 1;
40    if (!city) return s.length + " ударов за сутки";
41    if (extra <= 0) return "Удар: " + city;
42    return city + " и ещё " + extra + " " + plural(extra, "удар", "удара", "ударов");
43  }
44  function plural(n, one, few, many) {
45    var m10 = n % 10, m100 = n % 100;
46    if (m10 === 1 && m100 !== 11) return one;
47    if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return few;
48    return many;
49  }
50
51  // есть ли уже раскрытая соседняя плашка (vpn/sub)? тогда уступаем — стартуем язычком
52  function siblingOpen() {
53    return !!document.querySelector(".vpn-nudge:not(.nudge-out), .sub-nudge:not(.nudge-out)");
54  }
55
56  function build(date, title) {
57    var cover = COVER_OVERRIDES[date] || "/assets/cover-" + date + ".png";
58    var href = "/news/" + date;
59    var d = document.createElement("div");
60    d.className = "art-nudge";
61    d.innerHTML =
62      '<button type="button" class="art-nudge-x" aria-label="Закрыть">×</button>' +
63      '<a class="art-nudge-lnk" href="' + href + '">' +
64        '<img class="art-nudge-img" src="' + cover + '" alt="Сводка за ' + rusDate(date) + '" loading="lazy">' +
65        '<div class="art-nudge-body">' +
66          '<div class="art-nudge-kick">📰 Свежая сводка · ' + rusDate(date) + '</div>' +
67          '<div class="art-nudge-t">' + title + '</div>' +
68          '<div class="art-nudge-cta">Читать сводку →</div>' +
69        '</div>' +
70      '</a>';
71    d.querySelector(".art-nudge-lnk").addEventListener("click", function () { track(); markSeen(date); });
72    return d;
73  }
74
75  function wireDock(card, date, startDocked) {
76    var xbtn = card.querySelector(".art-nudge-x");
77    var dockFn = window.__nudgeDock;
78    if (dockFn) {
79      var d = dockFn(card, { key: K, label: "Свежая сводка", icon: NEWS_ICON, side: "right",
80                             pos: "top:60%", accent: "#d23a2e", startDocked: !!startDocked });
81      xbtn.addEventListener("click", d.collapse);
82    } else {
83      xbtn.addEventListener("click", function () { markSeen(date); card.remove(); });
84    }
85  }
86
87  // газета-иконка для язычка (сибling к щиту VPN / самолётику подписки)
88  var NEWS_ICON =
89    '<svg class="guard-face" viewBox="0 0 26 26" aria-hidden="true">' +
90      '<circle class="sh-body" cx="13" cy="13" r="11"/>' +
91      '<g class="face" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">' +
92        '<rect x="8" y="8" width="10" height="10" rx="1.2"/>' +
93        '<path d="M10 11h6M10 13.5h6M10 16h4"/>' +
94      '</g>' +
95    '</svg>';
96
97  var shown = false;
98  function show(date, title, forceDock) {
99    if (shown) return;
100    shown = true;
101    var d = build(date, title);
102    // обложки нет (404) → не показываем сломанную карточку
103    d.querySelector(".art-nudge-img").addEventListener("error", function () {
104      d.remove();
105    });
106    var dock = forceDock || siblingOpen();
107    document.body.appendChild(d);
108    if (dock) {
109      d.classList.add("in"); d.classList.add("nudge-out"); // сразу язычком
110      wireDock(d, date, true);
111    } else {
112      wireDock(d, date, false);
113      void d.offsetWidth; // форс-reflow → переход играет надёжно даже в фоновой вкладке
114      d.classList.add("in");
115    }
116  }
117
118  function css() {
119    var s = document.createElement("style");
120    s.textContent =
121      ".art-nudge{position:fixed;right:0;top:78px;z-index:1250;width:290px;max-width:calc(100vw - 28px);" +
122      "background:var(--surface,#fff);border:1px solid var(--line,#e4e4e7);border-right:none;" +
123      "border-radius:14px 0 0 14px;box-shadow:0 8px 30px rgba(0,0,0,.22);overflow:hidden;" +
124      "transform:translateX(115%);transition:transform .45s cubic-bezier(.22,1,.36,1)}" +
125      ".art-nudge.in{transform:translateX(0)}" +
126      ".art-nudge.nudge-out{transform:translateX(115%)}" +
127      ".art-nudge-x{position:absolute;top:6px;right:9px;z-index:2;background:rgba(0,0,0,.45);border:none;" +
128      "color:#fff;font-size:16px;line-height:1;cursor:pointer;padding:1px 7px;border-radius:20px}" +
129      ".art-nudge-x:hover{background:rgba(0,0,0,.7)}" +
130      ".art-nudge-lnk{display:block;text-decoration:none;color:inherit}" +
131      ".art-nudge-img{display:block;width:100%;height:150px;object-fit:cover;background:var(--surface2,#f0f0f0)}" +
132      ".art-nudge-body{padding:11px 14px 13px}" +
133      ".art-nudge-kick{color:var(--red,#d23a2e);font-size:10.5px;font-weight:800;letter-spacing:.3px;text-transform:uppercase}" +
134      ".art-nudge-t{color:var(--ink,#111);font-size:14px;font-weight:800;line-height:1.28;margin:5px 0 8px;font-family:var(--disp,inherit)}" +
135      ".art-nudge-cta{color:var(--teal,#12a594);font-size:12.5px;font-weight:800}" +
136      ".art-nudge-lnk:hover .art-nudge-cta{text-decoration:underline}" +
137      "@media(max-width:560px){.art-nudge{right:10px;left:10px;top:10px;width:auto;border-radius:14px;border-right:1px solid var(--line,#e4e4e7);" +
138      "transform:translateY(-140%)}.art-nudge.in{transform:translateY(0)}.art-nudge.nudge-out{transform:translateY(-140%)}" +
139      ".art-nudge-img{height:130px}}";
140    document.head.appendChild(s);
141  }
142
143  function init() {
144    css();
145    fetch("/data/news-archive.json", { cache: "no-store" })
146      .then(function (r) { return r.ok ? r.json() : null; })
147      .then(function (j) {
148        if (!j || !j.briefs) return;
149        var dates = Object.keys(j.briefs).sort().reverse();
150        resolveCover(dates, j.briefs, 0);
151      })
152      .catch(function () {});
153  }
154
155  // Свежайшая дата, у которой РЕАЛЬНО есть обложка: сводка Гермеса может опережать
156  // генерацию обложки (build-covers). Пре-probe детач-картинкой — показываем карточку
157  // только когда обложка загрузилась (значит и в DOM отрисуется мгновенно, из кеша).
158  // ponytail: окно отставания обложки ≤1 сутки → 3 попытки с запасом.
159  function resolveCover(dates, briefs, i) {
160    if (i >= dates.length || i >= 3) return; // обложек нет в разумном окне → карточку не показываем
161    var date = dates[i];
162    var probe = new Image();
163    probe.onload = function () { dispatch(date, makeTitle(briefs[date])); };
164    probe.onerror = function () { resolveCover(dates, briefs, i + 1); };
165    probe.src = COVER_OVERRIDES[date] || "/assets/cover-" + date + ".png";
166  }
167
168  function dispatch(date, title) {
169    var st = seenState();
170    var docked = (st === "seen:" + date || st === "dock"); // видел эту дату / ранее свернул → язычком
171    arm(date, title, docked);
172  }
173
174  // показать после первого взаимодействия (пан/скролл/клик) или через 10с —
175  // ловим «вовлечённого, но собравшегося уйти» читателя, не долбим сразу
176  function arm(date, title, forceDock) {
177    if (forceDock) { show(date, title, true); return; }
178    var fired = false, t;
179    function trigger() {
180      if (fired) return;
181      fired = true;
182      document.removeEventListener("pointerdown", trigger, true);
183      document.removeEventListener("wheel", trigger, { passive: true });
184      window.removeEventListener("scroll", trigger, { passive: true });
185      clearTimeout(t);
186      show(date, title, false);
187    }
188    document.addEventListener("pointerdown", trigger, true);
189    document.addEventListener("wheel", trigger, { passive: true });
190    window.addEventListener("scroll", trigger, { passive: true });
191    t = setTimeout(trigger, 10000);
192  }
193
194  if (document.readyState !== "loading") init();
195  else document.addEventListener("DOMContentLoaded", init);
196})();

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.