PageSourceSearch

https://allvols.com/js/ticketmaster-events.js

js allvols.com collected 2026-10-04 12:22:37 UTC 13,846 bytes, 353 lines download raw bytes

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.