PageSourceSearch

https://repression.net/_import/components/profileInspectorSql.0f23bf7e.js

js repression.net collected 2026-09-26 02:15:47 UTC 10,832 bytes, 269 lines download raw bytes

1import { html } from "../../_npm/[email protected]/11521f02.js";
2import markdownit from "../../_npm/[email protected]/173851b8.js";
3import { getPersonalCase } from "../data/queries.a9520730.js"
4import { declOfNum, removeColonUrlParams, getVestochkaUrl } from "./helpers.de78684b.js"
5
6const Markdown = new markdownit({html: true});
7
8function md(strings) {
9  let string = strings[0];
10  for (let i = 1; i < arguments.length; ++i) {
11    string += String(arguments[i]);
12    string += strings[i];
13  }
14  const template = document.createElement("template");
15  template.innerHTML = Markdown.render(string);
16  return template.content.cloneNode(true);
17}
18
19const STATE_CONFIG = {
20  'At large':            { cls: 'green',  labelRu: 'На свободе',     labelEn: 'At large' },
21  'Under investigation': { cls: 'orange', labelRu: 'Под следствием', labelEn: 'Under investigation' },
22  'Serving a sentence':  { cls: 'red',    labelRu: 'Отбывает наказание', labelEn: 'Serving a sentence' },
23  'Imprisoned':          { cls: 'red',    labelRu: 'В заключении',   labelEn: 'Imprisoned' },
24  'Not in Russia':       { cls: 'gray',   labelRu: 'Не в России',    labelEn: 'Not in Russia' },
25}
26const getStateConfig = state => STATE_CONFIG[state] ?? { cls: 'gray', labelRu: 'Неизвестно', labelEn: 'Unknown' }
27
28const STATE_COLOR = { green: '#10b981', orange: '#f97316', red: '#ef4444', gray: '#9ca3af' }
29
30function renderTimeline(item, lang) {
31  const startYear   = item.started_at   ? new Date(item.started_at).getUTCFullYear()   : null
32  const verdictYear = item.verdict_date ? new Date(item.verdict_date).getUTCFullYear() : null
33  const endedYear   = item.ended_at     ? new Date(item.ended_at).getUTCFullYear()     : null
34  const now         = new Date().getUTCFullYear()
35  const isActual    = !!item.actual
36
37  if (!startYear) return null
38
39  const L = {
40    start:   lang === 'ru' ? 'Начало'   : 'Start',
41    verdict: lang === 'ru' ? 'Приговор' : 'Verdict',
42    end:     lang === 'ru' ? 'Конец'    : 'End',
43    now:     lang === 'ru' ? 'сейчас'   : 'now',
44  }
45
46  const yrsLabel = n => `${n}\u00a0${lang === 'ru' ? declOfNum(n, ['год', 'года', 'лет']) : (n === 1 ? 'yr' : 'yrs')}`
47
48  const finalYear = isActual ? now : (endedYear ?? verdictYear ?? now)
49  const phase1End = verdictYear ?? finalYear
50  const p1        = Math.max(phase1End - startYear, 1)
51  const p2        = (verdictYear && isActual) ? Math.max(now - verdictYear, 1) : 0
52  const total     = p1 + p2 || 1
53
54  const pct1 = `${(p1 / total * 100).toFixed(1)}%`
55  const pct2 = `${(p2 / total * 100).toFixed(1)}%`
56
57  const stateCfg   = getStateConfig(item.person_state_en ?? item.state_en)
58  const stateColor = STATE_COLOR[stateCfg.cls] ?? '#9ca3af'
59
60  const p1Yrs = phase1End - startYear
61  const p2Yrs = now - (verdictYear ?? now)
62  const throughLabel = p1Yrs > 0
63    ? (lang === 'ru' ? `Через\u00a0${yrsLabel(p1Yrs)}` : `After\u00a0${yrsLabel(p1Yrs)}`)
64    : null
65
66  const tl = document.createElement('div')
67  tl.className = 'timeline'
68
69  tl.innerHTML = `
70    <div class="tl-bar" style="padding-right:12px">
71      <span class="tl-dot"></span>
72      <span class="tl-line phase1" style="width:${pct1}"></span>
73      ${verdictYear ? `<span class="tl-dot verdict"></span>` : ''}
74      ${p2 >
74 0 ? `<span class="tl-line phase2" style="width:${pct2};--sc:${stateColor}"></span>` : ''}
75      <span class="tl-dot end${isActual ? ' active' : ''}" ${isActual ? `style="--sc:${stateColor}"` : ''}></span>
76    </div>
77  `
78
79  const labels = document.createElement('div')
80  labels.className = 'tl-labels'
81
82  const startSpan = document.createElement('span')
83  startSpan.className = 'tl-start'
84  startSpan.innerHTML = `<b>${startYear}</b><span>${L.start}</span>`
85
86  const midSpan = verdictYear
87    ? html`<span class="tl-mid">
88        <b>${L.verdict}: ${verdictYear}</b>
89        ${throughLabel ? html`<span class="tl-dur">${throughLabel}</span>` : ''}
90      </span>`
91    : html`<span class="tl-mid"></span>`
92
93  const endSpan = isActual
94    ? html`<span class="tl-end" style="color:${stateColor}">
95        <b>${L.now}</b>
96        ${p2Yrs > 0 ? html`<span class="tl-dur">${lang === 'ru' ? `Спустя\u00a0${yrsLabel(p2Yrs)}` : `After\u00a0${yrsLabel(p2Yrs)}`}</span>` : ''}
97      </span>`
98    : (endedYear
99        ? html`<span class="tl-end"><b>${endedYear}</b><span>${L.end}</span></span>`
100        : html`<span class="tl-end"></span>`)
101
102  labels.appendChild(startSpan)
103  labels.appendChild(midSpan)
104  labels.appendChild(endSpan)
105  tl.appendChild(labels)
106
107  return tl
108}
109
110function renderSentence(item, lang) {
111  if (!item.verdict_essence) return null
112
113  const prefix = lang === 'ru' ? 'Приговор:\u00a0' : 'Sentence:\u00a0'
114  const el = document.createElement('div')
115  el.className = 'drawer-sentence'
116  el.innerHTML = `<b class="sentence-prefix">${prefix}</b>${item.verdict_essence}`
117  return el
118}
119
120function getDrawer() {
121  const existing = document.getElementById("_profile-drawer");
122  if (existing) return existing;
123
124  const overlay = document.createElement("div");
125  overlay.id = "_profile-overlay";
126
127  const drawer = document.createElement("div");
128  drawer.id = "_profile-drawer";
129
130  const closeBtn = document.createElement("button");
131  closeBtn.id = "_profile-close";
132  closeBtn.textContent = "✕";
133  closeBtn.title = "Закрыть";
134
135  const content = document.createElement("div");
136  content.id = "_profile-content";
137
138  const letterFooter = document.createElement("div");
139  letterFooter.id = "_profile-letter-footer";
140  letterFooter.style.display = "none";
141
142  const footerCloseBtn = document.createElement("button");
143  footerCloseBtn.id = "_profile-footer-close";
144  footerCloseBtn.title = "Закрыть";
145  footerCloseBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg><span>Закрыть</span>`;
146  letterFooter.appendChild(footerCloseBtn);
147
148  drawer.appendChild(closeBtn);
149  drawer.appendChild(content);
150  drawer.appendChild(letterFooter);
151
152  const helpBtn = () => document.querySelector("._oi_help_btn");
153  const close = () => {
154    overlay.classList.remove("open");
155    drawer.classList.remove("open");
156    removeColonUrlParams();
157    const btn = helpBtn(); if (btn) btn.style.visibility = "";
158  };
159
160  overlay.addEventListener("click", close);
161  closeBtn.addEventListener("click", close);
162  footerCloseBtn.addEventListener("click", close);
163  document.addEventListener("keydown", e => { if (e.key === "Escape") close(); });
164
165  document.body.appendChild(overlay);
166  document.body.appendChild(drawer);
167
168  return drawer;
169}
170
171export const profileInspectorSql = async (personId, options) => {
172  const { lang, urlParam } = options;
173  const drawer = getDrawer();
174  const overlay = document.getElementById("_profile-overlay");
175  const content = document.getElementById("_profile-content");
176  const letterFooter = document.getElementById("_profile-letter-footer");
177
178  const closeLabel = lang === 'en' ? 'Close' : 'Закрыть';
179  const closeBtn = document.getElementById("_profile-close");
180  if (closeBtn) closeBtn.title = closeLabel;
181  const footerCloseBtn = document.getElementById("_profile-footer-close");
182  if (footerCloseBtn) {
183    footerCloseBtn.title = closeLabel;
184    const span = footerCloseBtn.querySelector('span');
185    if (span) span.textContent = closeLabel;
186  }
187
188  if (personId === null) {
189    drawer.classList.remove("open");
190    overlay.classList.remove("open");
191    letterFooter.style.display = "none";
192    return document.createElement("div");
193  }
194
195  const data = await getPersonalCase(personId);
196  if (!data) return document.createElement("div");
197  content.innerHTML = "";
198
199  // Photo
200  if (data.photo) {
201    const img = document.createElement("img");
202    img.className = "drawer-photo";
203    img.src = data.photo;
204    content.appendChild(img);
205  }
206
207  // Name, age/region, status
208  const fragments = [];
209  if (data.birth_date) {
210    const age = Math.abs(new Date(Date.now() - new Date(data.birth_date)).getUTCFullYear() - 1970);
211    fragments.push(`${age}\u00a0${lang === 'ru' ? declOfNum(age, ["год", "года", "лет"]) : 'y.o.'}`);
212  }
213  if (data.region) {
214    const region = data.region?.toJSON ? data.region.toJSON() : String(data.region);
215    fragments.push(region);
216  }
217
218  const stateCfg   = getStateConfig(data.person_state_en ?? data.state_en);
219  const stateLabel = lang === 'ru' ? stateCfg.labelRu : stateCfg.labelEn;
220
221  content.appendChild(html`<div class="drawer-info person-info">
222    <p class="drawer-name">${data.name}</p>
223    ${fragments.length ? html`<p class="drawer-meta">${fragments.join(' · ')}</p>` : ''}
224    <p class="location"><span class="state ${stateCfg.cls}"></span>${stateLabel}</p>
225    ${(data.penal_facility || data.detention_center) ? html`<p class="facility">${data.penal_facility || data.detention_center}</p>` : ''}
226  </div>`);
227
228  // Timeline
229  const timeline = renderTimeline(data, lang);
230  if (timeline) content.appendChild(timeline);
231
232  // Sentence
233  const sentence = renderSentence(data, lang);
234  if (sentence) content.appendChild(sentence);
235
236  // Story
237  if (data.story) {
238    const storyEl = document.createElement("div");
239    storyEl.className = "drawer-story";
240    storyEl.appendChild(md`${data.story}`);
241    content.appendChild(storyEl);
242  }
243
244  // Letter footer
245  letterFooter.querySelector(".drawer-letter-btn")?.remove();
246  const footerClose = document.getElementById("_profile-footer-close");
247  if (data.letters && data.slug) {
248    const btnLabel = lang === 'ru' ? 'Написать письмо' : 'Write a letter';
249    const letterBtn = html`<a href="${getVestochkaUrl(data.slug, 'en')}" target="_blank" rel="noopener noreferrer" class="drawer-letter-btn">
250      <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
251      ${btnLabel}
252    </a>`;
253    letterFooter.insertBefore(letterBtn, footerClose);
254  }
255  letterFooter.style.display = "";
256
257  overlay.classList.add("open");
258  drawer.classList.add("open");
259  const helpBtnEl = document.querySelector("._oi_help_btn"); if (helpBtnEl) helpBtnEl.style.visibility = "hidden";
260
261  if (urlParam && data.person_id) {
262    const [base, search] = window.location.href.split('?')
263    const parts = (search || '').split('&').filter(p => p && !p.startsWith(`${urlParam}:`))
264    parts.push(`${urlParam}:${data.person_id}`)
265    history.pushState({}, "", `${base}?${parts.join('&')}`)
266  }
267
268  return document.createElement("div");
269}

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.