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 {'&':'&','<':'<','>':'>','"':'"'}[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.