1/* ========================================================================== 2 TICKETMASTER EVENTS â AllVols.com "Upcoming Events" 3 Pulls the next 6 upcoming Tennessee Athletics ticketed events directly 4 from the Ticketmaster Discovery API and renders them into the 5 "Upcoming Events" tables, so the list stays current without hand-editing 6 index.html for every new game. 7 8 Filtered by ATTRACTION ID (the team), not venue â this keeps non-Vols 9 events at our venues (concerts, etc.) out of the list. 10 11 SETUP: 12 1. TM_API_KEY is set below. 13 2. ATTRACTION_IDS holds one entry per ticketed sport. Add/remove sports 14 by editing this list. 15 ========================================================================== */ 16 17(function () { 18 "use strict"; 19 20 // ---- CONFIG ------------------------------------------------------------- 21 var TM_API_KEY = "EWBS4nxmUxlDWKtYfkfDcJSuGrkGacMx"; 22 23 // Attraction IDs for each ticketed Tennessee Athletics sport. 24 var ATTRACTION_IDS = [ 25 "K8vZ9171lh0", // Football 26 "K8vZ91724f7", // Baseball 27 "K8vZ9178mX7", // Softball 28 "K8vZ9171Oo0", // Women's Basketball 29 "K8vZ9171tX7", // Men's Basketball 30 "K8vZ9175hpf", // Volleyball 31 "K8vZ917417V" // Soccer 32 ]; 33 34 // Same IDs, mapped to a clean display name for each sport â used to 35 // rebuild a short "Tennessee [Sport] vs. [Opponent]" title instead of 36 // Ticketmaster's long raw event name. 37 var ATTRACTION_SPORTS = { 38 "K8vZ9171lh0": "Football", 39 "K8vZ91724f7": "Baseball", 40 "K8vZ9178mX7": "Softball", 41 "K8vZ9171Oo0": "Women's Basketball", 42 "K8vZ9171tX7": "Men's Basketball", 43 "K8vZ9175hpf": "Volleyball", 44 "K8vZ917417V": "Soccer" 45 }; 46 47 var MAX_EVENTS = 6; // total events to display (3 left / 3 right) 48 var FETCH_SIZE = 30; // pull extra so we still have 6 after filtering/de-duping 49 50 // Ticketmaster occasionally lists a placeholder/hold event that isn't a 51 // real scheduled game (no opponent, name is just "Tennessee Vols 52 // Football", etc.). Paste the event's Ticketmaster event ID here to hide 53 // it â grab the ID from the tail of its "BUY NOW" link, e.g. 54 // ".../event/160064F1C06EBEDA" -> "160064F1C06EBEDA". 55 var EXCLUDED_EVENT_IDS = [ 56 "Z7r9jZ1A70O8-", // Phantom "Tennessee Vols Football" placeholder, no real Sept. 4 game 57 ]; 58 59 // Manual overrides for opponent display names. Mascots aren't formatted 60 // consistently by Ticketmaster (some events include them, some don't), 61 // so there's no reliable generic rule to strip them â add an entry here 62 // any time an opponent's name still looks off. The key just needs to be 63 // a distinctive lowercase word/phrase found in Ticketmaster's raw event 64 // name; the value is exactly what you want displayed. 65 var OPPONENT_OVERRIDES = { 66 "illinois": "Illinois", 67 "morehead state": "Morehead State" 68 }; 69 70 // Only show HOME events. Away games (e.g. Volleyball @ Cincinnati) show 71 // up in attractionId results too since they involve a Tennessee team, 72 // so we filter down to events at our own venues by city. 73 var HOME_CITY = "Knoxville"; 74 75 var EVENTS_URL = 76 "https://app.ticketmaster.com/discovery/v2/events.json" + 77 "?apikey=" + encodeURIComponent(TM_API_KEY) + 78 "&attractionId=" + ATTRACTION_IDS.map(encodeURIComponent).join(",") + 79 "&size=" + FETCH_SIZE + 80 "&sort=date,asc"; 81 82 var MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; 83 84 // ---- DOM HOOKS ------------------------------------------------------------ 85 // These <tbody> elements live inside the two "Upcoming Events" tables 86 // in index.html (#text-right). Left table fills first, remaining events 87 // go right. 88 function getBodies() { 89 return { 90 left: document.getElementById("events-left-body"), 91 right: document.getElementById("events-right-body") 92 }; 93 } 94 95 document.addEventListener("DOMContentLoaded", init); 96
97 function init() { 98 var bodies = getBodies(); 99 if (!bodies.left || !bodies.right) { 100 console.warn("Ticketmaster events: expected #events-left-body / #events-right-body not found."); 101 return; 102 } 103 104 if (!TM_API_KEY) { 105 renderMessage(bodies.left, "Event calendar is not yet configured."); 106 return; 107 } 108 109 fetchEvents(bodies); 110 } 111 112 function fetchEvents(bodies) { 113 fetch(EVENTS_URL) 114 .then(function (res) { 115 if (!res.ok) { 116 throw new Error("Ticketmaster API responded with " + res.status); 117 } 118 return res.json(); 119 }) 120 .then(function (data) { 121 var events = (data && data._embedded && data._embedded.events) || []; 122 var homeEvents = dedupeEvents(events.filter(isHomeEvent).filter(isNotExcluded)); 123 if (!homeEvents.length) { 124 renderMessage(bodies.left, "No upcoming home events at this time. Check back soon!"); 125 return; 126 } 127 renderEvents(bodies, homeEvents.slice(0, MAX_EVENTS)); 128 }) 129 .catch(function (err) { 130 console.error("Failed to load Ticketmaster events:", err); 131 renderMessage( 132 bodies.left, 133 'Unable to load the event calendar right now. View events directly on <a href="https://www.ticketmaster.com/" target="_blank" rel="noopener">Ticketmaster</a>.' 134 ); 135 }); 136 } 137 138 // Keeps only events happening in Knoxville (i.e. at one of our own 139 // venues), filtering out road games against the same attraction IDs. 140 function isHomeEvent(event) { 141 var venues = event && event._embedded && event._embedded.venues; 142 if (!venues || !venues.length) { 143 return false; 144 } 145 return venues.some(function (venue) { 146 return venue && venue.city && venue.city.name === HOME_CITY; 147 }); 148 } 149 150 // Filters out any event ID listed in EXCLUDED_EVENT_IDS above. 151 function isNotExcluded(event) { 152 return !event || !event.id || EXCLUDED_EVENT_IDS.indexOf(event.id) === -1; 153 } 154 155 // Ticketmaster can return the same event twice when it matches more than 156 // one attraction ID (e.g. a game tagged to two teams). Keep only the 157 // first occurrence of each event id. 158 function dedupeEvents(events) { 159 var seen = {}; 160 return events.filter(function (event) { 161 if (!event.id || seen[event.id]) { 162 return false; 163 } 164 seen[event.id] = true; 165 return true; 166 }); 167 } 168 169 function renderEvents(bodies, events) { 170 bodies.left.innerHTML = ""; 171 bodies.right.innerHTML = ""; 172 173 // Split evenly across the two columns, left-heavy (3 / 3 with 6 events). 174 var half = Math.ceil(events.length / 2); 175 176 events.forEach(function (event, index) { 177 var row = buildEventRow(event); 178 if (index < half) { 179 bodies.left.appendChild(row); 180 } else { 181 bodies.right.appendChild(row); 182 } 183 }); 184 } 185 186 function buildEventRow(event) { 187 var tr = document.createElement("tr"); 188 189 var infoCell = document.createElement("td"); 190 var h2 = document.createElement("h2"); 191 h2.textContent = formatEventTitle(event); 192 var p = document.createElement("p"); 193 p.textContent = formatEventDate(event); 194 infoCell.appendChild(h2); 195 infoCell.appendChild(p); 196 197 var buttonCell = document.createElement("td"); 198 var eventUrl = getEventUrl(event); 199 if (eventUrl) { 200 var buyLink = document.createElement("a"); 201 buyLink.href = eventUrl; 202 buyLink.target = "_blank"; 203 buyLink.rel = "noopener"; 204 buyLink.className = "button-link-orange"; 205 buyLink.textContent = "BUY NOW"; 206 buttonCell.appendChild(buyLink); 207 } 208 209 tr.appendChild(infoCell); 210 tr.appendChild(buttonCell); 211 return tr; 212 } 213 214 // Ticketmaster's affiliate wrapper (ticketmaster.evyy.net) has been 215 // returning malformed redirects for some events. The real destination 216 // is stored in that link's own "u" query parameter, so we pull it out 217 // and link straight to Ticketmaster instead. Note: this means clicks 218 // won't be credited through the affiliate program. 219 function getEventUrl(event) { 220 var raw = event && event.url; 221 if (!raw) { 222 return null; 223 } 224 try { 225 var parsed = new URL(raw); 226 var real = parsed.searchParams.get("u"); 227 if (real) { 228 return real; 229 } 230 } catch (e) { 231 // raw wasn't a parseable absolute URL - fall through to using it as-is 232 } 233 return raw; 234 } 235 236 function formatEventDate(event) { 237 var start = event && event.dates && event.dates.start; 238 if (!start || !start.localDate) { 239 return "DATE TBA"; 240 } 241 242 var parts = start.localDate.split("-").map(Number); // [YYYY, MM, DD]
243 var year = parts[0], month = parts[1], day = parts[2]; 244 return MONTHS[month - 1] + ". " + day + ", " + year; 245 } 246 247 // Rebuilds Ticketmaster's long raw event name (e.g. "TENNESSEE 248 // VOLUNTEERS VOLLEYBALL VS. COLGATE UNIVERSITY WOMEN'S VOLLEYBALL") 249 // into a short "Tennessee Volleyball vs. Colgate" style title. 250 // Falls back to a title-cased version of the raw name if it can't 251 // confidently identify the sport and opponent. 252 function formatEventTitle(event) { 253 var sport = getEventSport(event); 254 var opponent = getOpponentName(event && event.name, sport); 255 256 if (sport && opponent) { 257 return "Tennessee " + sport + " vs. " + opponent; 258 } 259 260 return titleCase((event && event.name) || ""); 261 } 262 263 // Finds the sport by checking which of our known attraction IDs is 264 // tagged to this event â more reliable than guessing from the name. 265 function getEventSport(event) { 266 var attractions = event && event._embedded && event._embedded.attractions; 267 if (!attractions) { 268 return null; 269 } 270 for (var i = 0; i < attractions.length; i++) { 271 var sport = ATTRACTION_SPORTS[attractions[i].id]; 272 if (sport) { 273 return sport; 274 } 275 } 276 return null; 277 } 278 279 // Pulls the opponent name out of the raw event name. Ticketmaster's
280 // college sports names are consistently "... VS. <OPPONENT> ..." with 281 // the opponent's mascot/sport tacked on afterward, often separated by 282 // the word "UNIVERSITY" â so we cut there when it's present. 283 function getOpponentName(rawName, sport) { 284 if (!rawName) { 285 return null; 286 } 287 288 var override = matchOpponentOverride(rawName); 289 if (override) { 290 return override; 291 } 292 293 var match = rawName.match(/\bVS\.?\s+(.+)/i); 294 if (!match) { 295 return null; 296 } 297 298 var opponent = match[1]; 299 300 // Handle "... VS. UNIVERSITY OF ILLINOIS FIGHTING ILLINI ..." â strip 301 // a leading "University of" so it doesn't eat the whole opponent name. 302 opponent = opponent.replace(/^UNIVERSITY OF\s+/i, ""); 303 304 if (/\bUNIVERSITY\b/i.test(opponent)) { 305 // "COLGATE UNIVERSITY ..." â school name comes before "University". 306 opponent = opponent.split(/\bUNIVERSITY\b/i)[0]; 307 } else if (/\b(WOMEN'?S|MEN'?S)\b/i.test(opponent)) { 308 // "MOREHEAD STATE EAGLES WOMEN'S VOLLEYBALL" â cut before the 309 // gender qualifier so the mascot/sport suffix drops off too. 310 opponent = opponent.split(/\b(?:WOMEN'?S|MEN'?S)\b/i)[0]; 311 } else if (sport) { 312 // No "University" or gender tag to cut at â at minimum strip a 313 // trailing repeat of the sport name. 314 var sportEscaped = sport.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 315 opponent = opponent.replace(new RegExp("\\b" + sportEscaped + "\\b\\s*$", "i"), ""); 316 } 317 318 opponent = opponent.replace(/\s+/g, " ").trim(); 319 return opponent ? titleCase(opponent) : null; 320 } 321 322 // Checks the raw event name against OPPONENT_OVERRIDES and returns the 323 // manual clean name if a key matches, otherwise null. 324 function matchOpponentOverride(rawName) { 325 var lower = rawName.toLowerCase(); 326 for (var key in OPPONENT_OVERRIDES) { 327 if (Object.prototype.hasOwnProperty.call(OPPONENT_OVERRIDES, key) && lower.indexOf(key) !== -1) { 328 return OPPONENT_OVERRIDES[key]; 329 } 330 } 331 return null; 332 } 333 334 // Common all-caps college athletics acronyms that should stay uppercase 335 // rather than being title-cased (e.g. "LSU", not "Lsu"). 336 var KEEP_UPPERCASE = ["LSU", "USC", "UCLA", "TCU", "SMU", "VMI", "UNC", "UAB", 337 "FIU", "UCF", "ETSU", "BYU", "VCU", "UNLV", "FAU", "FGCU", "ULM", "UTEP", "UTSA", "A&M"]; 338 339 function titleCase(str) { 340 return str.split(/\s+/).map(function (word) { 341 var bare = word.replace(/[^A-Z&]/gi, "").toUpperCase(); 342 if (KEEP_UPPERCASE.indexOf(bare) !== -1) { 343 return word.toUpperCase(); 344 } 345 return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); 346 }).join(" "); 347 } 348 349 function renderMessage(target, html) { 350 if (!target) return; 351 target.innerHTML = '<tr><td colspan="2"><p>' + html + "</p></td></tr>"; 352 } 353})();
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.