PageSourceSearch

https://hctorpedo.ru/js/events-loader.js

js hctorpedo.ru collected 2026-09-24 19:49:43 UTC 14,092 bytes, 358 lines download raw bytes

1/**
2 * HC Torpedo — "Ближайшие события" dynamic loader
3 *
4 * Layout logic:
5 *   - Today slide is positioned at center (3 past days visible to the left)
6 *   - Today slide gets red border (_today class)
7 *   - Future slides 1-3 are visible normally
8 *   - Future slides 4+ get blur class (_blur) + right gradient fade overlay
9 *
10 * Multi-event days:
11 *   - 1 event  → normal slide (width = 1 unit)
12 *   - 2 events → _double slide (width = 2 units), two events side by side
13 *   - 3+ events → _triple slide (width = 3 units), first 3 events shown
14 *
15 * Slide widths (matching gorky/css/main.css breakpoints):
16 *   < 768px  → 128px
17 *   ≥ 768px  → 188px
18 *   ≥ 1200px → 1/6 of container width
19 */
20
21(function () {
22  var DAYS_BACK         = 3;
23  var DAYS_AHEAD        = 14;
24  var FUTURE_VISIBLE    = 3;   // future days shown without blur
25  var RU_DAYS   = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'];
26  var RU_MONTHS = ['Января','Февраля','Марта','Апреля','Мая','Июня',
27                   'Июля','Августа','Сентября','Октября','Ноября','Декабря'];
28
29  function toDateKey(d) {
30    return d.getFullYear() + '-'
31      + String(d.getMonth() + 1).padStart(2, '0') + '-'
32      + String(d.getDate()).padStart(2, '0');
33  }
34
35  function esc(s) {
36    return String(s || '').replace(/[&<>"]/g, function (c) {
37      return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];
38    });
39  }
40
41  function initials(name) {
42    return String(name || '').split(/\s+/).filter(Boolean)
43      .map(function (w) { return w[0].toUpperCase(); }).slice(0, 2).join('') || '?';
44  }
45
46  function logoImg(url, name, cls) {
47    if (!url) return '';
48    return '<img class="' + cls + '" src="' + esc(url) + '" alt="' + esc(name) + '" data-name="' + esc(name) + '">';
49  }
50
51  function phDiv(name, small) {
52    var fs = small ? '.85rem' : '2rem';
53    return '<div style="width:100%;height:100%;display:flex;align-items:center;'
54      + 'justify-content:center;font-weight:700;font-size:' + fs
55      + ';color:rgba(255,255,255,.35)">' + esc(initials(name)) + '</div>';
56  }
57
58  function attachFallbacks(container) {
59    container.querySelectorAll('img[data-name]').forEach(function (img) {
60      if (img.complete && img.naturalWidth === 0) {
61        img.parentNode && img.parentNode.replaceChild(
62          makePh(img.getAttribute('data-name'), img.classList.contains('events__logo')),
63          img
64        );
65        return;
66      }
67      img.addEventListener('error', function () {
68        var isLogo = img.classList.contains('events__logo');
69        var ph = makePh(img.getAttribute('data-name') || '', isLogo);
70        img.parentNode && img.parentNode.replaceChild(ph, img);
71      });
72    });
73  }
74
75  function makePh(name, small) {
76    var el = document.createElement('div');
77    el.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;'
78      + 'justify-content:center;font-weight:700;color:rgba(255,255,255,.35);'
79      + 'font-size:' + (small ? '.85rem' : '2rem');
80    el.textContent = initials(name);
81    return el;
82  }
83
84  /** Inner HTML of events__content for empty day */
85  function emptyContent(d) {
86    return '<div class="events__info">'
87      +   '<div class="events__day">' + RU_DAYS[d.getDay()] + '</div>'
88      +   '<div class="events__date">' + d.getDate() + ' <span class="events__month">' + RU_MONTHS[d.getMonth()] + '</span></div>'
89      +   '<div class="events__type" style="opacity:.4">Нет событий</div>'
90      + '</div>';
91  }
92
93  /** Inner HTML of events__content for a match */
94  function matchContent(m, d, showDate) {
95    var isHome    = !!m.is_home;
96    var typeClass = isHome ? '_dark' : '_red';
97    var typeBg    = isHome ? ' style="background:#00205b"' : '';
98    var typeLabel = isHome ? 'Домашний матч' : 'Гостевой матч';
99    var hh = String(d.getHours()).padStart(2, '0');
100    var mm = String(d.getMinutes()).padStart(2, '0');
101
102    return '<div class="events__match">'
103      +   '<span class="events__logo-container">'
104      +     (m.home_team_logo ? logoImg(m.home_team_logo, m.home_team_name, 'events__logo') : phDiv(m.home_team_name, true))
105      +   '</span>'
106      +   '<div class="events__match-label">Vs</div>'
107      +   '<span class="events__logo-container">'
108      +     (m.away_team_logo ? logoImg(m.away_team_logo, m.away_team_name, 'events__logo') : phDiv(m.away_team_name, true))
109      +   '</span>'
110      + '</div>'
111      + '<div class="events__info">'
112      +   '<div class="events__type ' + typeClass + '"' + typeBg + '>' + typeLabel + '</div>'
113      +   (showDate ? '<div class="events__day">' + RU_DAYS[d.getDay()] + '</div>' : '')
114      +   (showDate ? '<div class="events__date">' + d.getDate() + ' <span class="events__month">' + RU_MONTHS[d.getMonth()] + '</span></div>' : '')
115      +   '<div class="events__time">' + hh + ':' + mm + '</div>'
116      + '</div>';
117  }
118
119  /** Resolve profile URL for a person based on role hint */
120  function personUrl(person) {
121    var slug = person.slug;
122    if (!slug) return null;
123    var role = person._role || '';
124    if (role === 'player') return '/teamroster/' + slug + '/';
125    if (role === 'legend') return '/legends/' + slug + '/';
126    // staff: coach → /coaches/, management|leader → /manage/
127    var type = person.type || '';
128    if (type === 'coach') return '/coaches/' + slug + '/';
129    if (type === 'management' || type === 'leader') return '/manage/' + slug + '/';
130    return null;
131  }
132
133  /** Inner HTML of events__content for a birthday */
134  function birthdayContent(person, d, showDate) {
135    var name = ((person.name || '') + ' ' + (person.lastname || '')).trim();
136    var img  = person.image || '';
137    var url  = personUrl(person);
138
139    return '<span class="events__img-container">'
140      +   (img ? logoImg(img, name, 'events__img') : phDiv(name, false))
141      + '</span>'
142      + '<div class="events__info">'
143      +   '<div class="events__type _blue">День Рождения</div>'
144      +   (showDate ? '<div class="events__day">' + RU_DAYS[d.getDay()] + '</div>' : '')
145      +   (showDate ? '<div class="events__date">' + d.getDate() + ' <span class="events__month">' + RU_MONTHS[d.getMonth()] + '</span></div>' : '')
146      +   (name ? '<div class="events__name">' + esc(name) + '</div>' : '')
147      + '</div>';
148  }
149
150  /**
151   * Build a single swiper-slide for a given day.
152   * events = array of { type: 'match'|'birthday', data, date }
153   */
154  function buildDaySlide(events, d, extraClass) {
155    var count  = Math.min(events.length, 3);
156    var sizeClass = count === 2 ? ' _double' : count === 3 ? ' _triple' : '';
157    var todayClass = extraClass ? ' ' + extraClass : '';
158
159    var html = '<div class="swiper-slide events__slide js-events-slide' + sizeClass + todayClass + '">';
160
161    if (count === 0) {
162      html += '<a class="events__content" draggable="false">' + emptyContent(d) + '</a>';
163    } else {
164      for (var i = 0; i < count; i++) {
165        var ev = events[i];
166        var showDate = (i === 0); // only first event in a day shows the date
167        var href = '';
168        if (ev.type === 'birthday') {
169          var bUrl = personUrl(ev.data);
170          if (bUrl) href = ' href="' + bUrl + '"';
171        }
172        html += '<a class="events__content" draggable="false"' + href + '>';
173        if (ev.type === 'match') {
174          html += matchContent(ev.data, ev.date, showDate);
175        } else {
176          html += birthdayContent(ev.data, d, showDate);
177        }
178        html += '</a>';
179      }
180    }
181
182    html += '</div>';
183    return html;
184  }
185
186  /** Compute slide width matching gorky/css/main.css breakpoints */
187  function baseSlideWidth(containerEl) {
188    var w = window.innerWidth;
189    if (w >= 1200) {
190      var containerW = containerEl ? containerEl.offsetWidth : w;
191      return Math.floor(containerW / 6);
192    }
193    if (w >= 768) return 188;
194    // Mobile: 128px matches main.css base → content 118×118 (original design)
195    return 128;
196  }
197
198  /** Force correct widths on all slides via inline style */
199  function applyWidths(wrapper, containerEl) {
200    var base = baseSlideWidth(containerEl);
201    wrapper.querySelectorAll('.events__slide').forEach(function (slide) {
202      var units = slide.classList.contains('_triple') ? 3
203                : slide.classList.contains('_double') ? 2
204                : 1;
205      // gap between adjacent cells = 2 * padding per side
206      // < 768px: padding 5px → gap 10px; ≥ 768px: padding 6px → gap 12px
207      var gap = window.innerWidth >= 768 ? 12 : 10;
208      var w = (base * units - gap * (units - 1)) + 'px';
209      slide.style.setProperty('width', w, 'important');
210      slide.style.setProperty('flex-shrink', '0', 'important');
211    });
212  }
213
214  function updateSwiper(swiperEl, wrapper, todaySlideIndex) {
215    if (!swiperEl) return;
216
217    applyWidths(wrapper, swiperEl);
218
219    var sw = swiperEl.swiper;
220    if (sw) {
221      sw.params.slidesPerView       = 'auto';
222      sw.params.watchSlidesProgress = true;
223      sw.params.slideVisibleClass   = '_visible';
224      sw.update();
225      // Mobile: slide today to the start (2nd position looks natural with peek)
226      // Desktop: show today as (DAYS_BACK+1)-th slide so past days are visible
227      var isMobile = window.innerWidth < 768;
228      if (isMobile) {
229        // Show today at position 1 (one past slide visible to the left)
230        sw.slideTo(Math.max(0, todaySlideIndex - 1), 0);
231      } else {
232        sw.slideTo(Math.max(0, todaySlideIndex - DAYS_BACK), 0);
233      }
234    }
235
236    applyWidths(wrapper, swiperEl);
237  }
238
239  /** Add gradient fade overlays on both sides of container */
240  function addFadeOverlay(containerEl) {
241    if (!containerEl) return;
242    if (!containerEl.querySelector('.events__fade-right')) {
243      var fadeR = document.createElement('div');
244      fadeR.className = 'events__fade-right';
245      containerEl.appendChild(fadeR);
246    }
247    if (!containerEl.querySelector('.events__fade-left')) {
248      var fadeL = document.createElement('div');
249      fadeL.className = 'events__fade-left';
250      containerEl.appendChild(fadeL);
251    }
252  }
253
254  function loadEvents() {
255    var wrapper = document.querySelector('.events__wrapper');
256    if (!wrapper) return;
257
258    Promise.all([
259      window.TorpedoApi.schedule({ days_back: DAYS_BACK, days_ahead: DAYS_AHEAD }).catch(function () { return null; }),
260      window.TorpedoApi.players({ per_page: 200 }).catch(function () { return null; }),
261      window.TorpedoApi.staff({ per_page: 200 }).catch(function () { return null; }),
262      window.TorpedoApi.legends({ club: 'torpedo', per_page: 200 }).catch(function () { return null; }),
263    ]).then(function (results) {
264      var rawMatches = (results[0] && results[0].data) ? results[0].data : [];
265      var players    = (results[1] && results[1].data) ? results[1].data : [];
266      var staff      = (results[2] && results[2].data) ? results[2].data : [];
267      var legends    = (results[3] && results[3].data) ? results[3].data : [];
268
269      // Index matches by date key
270      var matchesByDate = {};
271      rawMatches.forEach(function (m) {
272        var key = (m.date || '').split('T')[0];
273        if (!matchesByDate[key]) matchesByDate[key] = [];
274        matchesByDate[key].push(m);
275      });
276
277      // Index birthdays by current-year date key (players + staff + legends)
278      var yr = new Date().getFullYear();
279      var birthsByDate = {};
280
281      // Tag each person with _role so birthdayContent can build the correct URL
282      players.forEach(function (p) { p._role = 'player'; });
283      staff.forEach(function (p)   { p._role = 'staff'; });
284      legends.forEach(function (p) { p._role = 'legend'; });
285
286      var allPeople = players.concat(staff).concat(legends);
287      allPeople.forEach(function (p) {
288        if (!p.birthdate) return;
289        var parts = String(p.birthdate).split('T')[0].split('-');
290        if (parts.length < 3) return;
291        var key = yr + '-' + parts[1] + '-' + parts[2];
292        if (!birthsByDate[key]) birthsByDate[key] = [];
293        birthsByDate[key].push(p);
294      });
295
296      var today = new Date();
297      today.setHours(0, 0, 0, 0);
298      var html = [];
299      var todaySlideIndex = 0;
300
301      for (var i = -DAYS_BACK; i <= DAYS_AHEAD; i++) {
302        var d   = new Date(today);
303        d.setDate(today.getDate() + i);
304        var key = toDateKey(d);
305
306        var extraClass = '';
307        if (i === 0) {
308          todaySlideIndex = html.length;
309          extraClass = '_today';
310        } else if (i > FUTURE_VISIBLE) {
311          extraClass = '_future-blur';
312        }
313
314        var dayMatches   = matchesByDate[key]  || [];
315        var dayBirthdays = birthsByDate[key]    || [];
316
317        // Build unified events list for this day: matches first, then birthdays
318        var dayEvents = [];
319        dayMatches.forEach(function (m) {
320          // window.parseMatchDate снимает Z с naive-local времени бэкенда.
321          var matchDate = m.date
322            ? (window.parseMatchDate ? window.parseMatchDate(m.date) : new Date(m.date))
323            : d;
324          dayEvents.push({ type: 'match', data: m, date: matchDate });
325        });
326        dayBirthdays.forEach(function (p) {
327          dayEvents.push({ type: 'birthday', data: p, date: d });
328        });
329
330        html.push(buildDaySlide(dayEvents, d, extraClass));
331      }
332
333      wrapper.innerHTML = html.join('');
334      wrapper.classList.add('_loaded');
335      attachFallbacks(wrapper);
336
337      var swiperEl = document.querySelector('.events__container');
338      updateSwiper(swiperEl, wrapper, todaySlideIndex);
339      addFadeOverlay(swiperEl);
340
341      // Re-apply on resize (debounced)
342      var resizeTimer;
343      window.addEventListener('resize', function () {
344        clearTimeout(resizeTimer);
345        resizeTimer = setTimeout(function () {
346          applyWidths(wrapper, swiperEl);
347          if (swiperEl && swiperEl.swiper) swiperEl.swiper.update();
348        }, 150);
349      });
350    });
351  }
352
353  if (document.readyState === 'loading') {
354    document.addEventListener('DOMContentLoaded', loadEvents);
355  } else {
356    loadEvents();
357  }
358})();

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.