1function escapeHtml(value) { 2 return String(value ?? "") 3 .replaceAll("&", "&") 4 .replaceAll("<", "<") 5 .replaceAll(">", ">") 6 .replaceAll('"', """) 7 .replaceAll("'", "'"); 8} 9 10function cleanInlineText(value = "") { 11 return String(value || "") 12 .replace(/\s+/g, " ") 13 .trim(); 14} 15 16function formatPostedAt(value) { 17 if (!value) return "Posted time pending"; 18 const date = new Date(value); 19 if (Number.isNaN(date.getTime())) return "Posted time pending"; 20 const formatted = new Intl.DateTimeFormat("en-US", { 21 month: "short", 22 day: "numeric", 23 year: "numeric", 24 hour: "numeric", 25 minute: "2-digit", 26 timeZone: "America/New_York", 27 timeZoneName: "short" 28 }).format(date).replace(/, (?=\d{1,2}:)/, " at "); 29 return `Posted ${formatted}`; 30} 31 32const HISTORY_NOW_LANE_FILTERS = [ 33 { key: "artifactsanddocuments", routePrefix: "ArtifactsandDocuments", label: "Artifacts & Documents", shortLabel: "Artifacts" }, 34 { key: "sportsbusinessorigins", routePrefix: "SportsBusinessOrigins", label: "Sports Business Origins", shortLabel: "Business Origins" }, 35 { key: "institutionaldesign", routePrefix: "InstitutionalDesign", label: "Institutional Design", shortLabel: "Institutional Design" }, 36 { key: "fancultureandvenuerituals", routePrefix: "FanCultureandVenueRituals", label: "Fan Culture & Venue Rituals", shortLabel: "Fan Rituals" }, 37 { key: "sportsscienceandtraininghistory", routePrefix: "SportsScienceandTrainingHistory", label: "Sports Science & Training History", shortLabel: "Science & Training" }, 38 { key: "globalnichesports", routePrefix: "GlobalNicheSports", label: "Global/Niche Sports", shortLabel: "Global/Niche" }, 39 { key: "failedfutures", routePrefix: "FailedFutures", label: "Failed Futures", shortLabel: "Failed Futures" } 40]; 41 42const ARTICLE_MONTHS = [ 43 { value: "01", label: "Jan" }, 44 { value: "02", label: "Feb" }, 45 { value: "03", label: "Mar" }, 46 { value: "04", label: "Apr" }, 47 { value: "05", label: "May" }, 48 { value: "06", label: "Jun" }, 49 { value: "07", label: "Jul" }, 50 { value: "08", label: "Aug" }, 51 { value: "09", label: "Sep" }, 52 { value: "10", label: "Oct" }, 53 { value: "11", label: "Nov" }, 54 { value: "12", label: "Dec" } 55]; 56 57function normalizeLaneKey(value = "") { 58 return cleanInlineText(value) 59 .toLowerCase() 60 .replace(/&/g, "and") 61 .replace(/[^a-z0-9]+/g, ""); 62} 63 64function laneFilterFor(value = "") { 65 const key = normalizeLaneKey(value); 66 if (!key) return null; 67 return HISTORY_NOW_LANE_FILTERS.find((lane) => 68 key === lane.key || 69 key === normalizeLaneKey(lane.routePrefix) || 70 key === normalizeLaneKey(lane.label) 71 ) || null; 72} 73 74function activeLane() { 75 return laneFilterFor(new URLSearchParams(location.search).get("lane") || "")?.routePrefix || ""; 76} 77 78function activeLaneMeta() { 79 return laneFilterFor(activeLane()); 80} 81 82function activeSport() { 83 return new URLSearchParams(location.search).get("sport")?.toUpperCase() || ""; 84} 85 86function activeType() { 87 const type = new URLSearchParams(location.search).get("type"); 88 if (type === "draft") return "draft"; 89 if (type === "evergreen") return "evergreen"; 90 if (type === "daily") return "daily"; 91 if (type === "archive" || type === "waiver") return "archive"; 92 return "all"; 93} 94 95function activeSignal() { 96 const signal = new URLSearchParams(location.search).get("signal"); 97 return signal === "breaking" || signal === "trending" ? signal : ""; 98} 99 100function activeSection() { 101 return cleanInlineText(new URLSearchParams(location.search).get("section") || "").slice(0, 80); 102} 103 104function normalizeArticleSort(value = "") { 105 return cleanInlineText(value).toLowerCase() === "oldest" ? "oldest" : "latest"; 106} 107 108function activeArticleSort(url = new URL(location.href)) { 109 return normalizeArticleSort(url.searchParams.get("sort") || ""); 110} 111 112function normalizeArticleYear(value = "") { 113 const year = Number.parseInt(String(value || ""), 10); 114 const maxYear = new Date().getFullYear() + 2; 115 return Number.isInteger(year) && year >= 1900 && year <= maxYear ? String(year) : ""; 116} 117 118function activeArticleYear(url = new URL(location.href)) { 119 return normalizeArticleYear(url.searchParams.get("year") || ""); 120} 121 122function normalizeArticleMonth(value = "") { 123 const raw = cleanInlineText(value).toLowerCase(); 124 const labelMatch = ARTICLE_MONTHS.find((month) => month.label.toLowerCase() === raw || month.label.toLowerCase().startsWith(raw)); 125 if (labelMatch && raw.length >= 3) return labelMatch.value;
126 const month = Number.parseInt(raw, 10); 127 return Number.isInteger(month) && month >= 1 && month <= 12 ? String(month).padStart(2, "0") : ""; 128} 129 130function activeArticleMonth(url = new URL(location.href)) { 131 return normalizeArticleMonth(url.searchParams.get("month") || ""); 132} 133 134function currentViewLabel() { 135 const type = activeType(); 136 const sport = activeSport(); 137 const signal = activeSignal(); 138 const section = activeSection(); 139 const lane = activeLaneMeta(); 140 if (section) return sport ? `${sport} ${section}` : section; 141 if (signal === "breaking") return sport ? `${sport} Breaking` : "Breaking"; 142 if (signal === "trending") return sport ? `${sport} Trending` : "Trending"; 143 if (lane) return lane.label; 144 if (type === "draft") return sport ? `${sport} Draft Lab` : "Draft Lab"; 145 if (type === "evergreen" && sport === "CULTURE") return "Culture"; 146 if (type === "evergreen") return sport ? `${sport} History` : "History"; 147 if (type === "daily") return sport ? `${sport} Daily Pulse` : "Daily Pulse"; 148 if (type === "archive") return sport ? `${sport} Waiver Archive` : "Waiver Archive"; 149 return sport ? `${sport} Articles` : "All Articles"; 150} 151 152function libraryLabel() { 153 const type = activeType(); 154 const sport = activeSport(); 155 const signal = activeSignal(); 156 const section = activeSection(); 157 const lane = activeLaneMeta(); 158 if (section) return section.toLowerCase(); 159 if (signal === "breaking") return "breaking news"; 160 if (signal === "trending") return "trending article"; 161 if (lane) return lane.label.toLowerCase(); 162 if (type === "evergreen" && sport === "CULTURE") return "sports culture"; 163 if (type === "evergreen") return "historical feature"; 164 if (type === "draft") return "draft lab"; 165 if (type === "daily") return "daily market pulse"; 166 if (type === "archive") return "waiver receipt"; 167 return "article"; 168} 169 170function sportQuery(sport = "") { 171 const value = String(sport || "").trim().toUpperCase(); 172 return value ? `?sport=${encodeURIComponent(value)}` : ""; 173} 174 175function sportDirectoryHash(sport = "") { 176 const value = String(sport || "").trim().toLowerCase(); 177 return value ? `#${encodeURIComponent(value)}-intelligence` : ""; 178} 179 180function sportDirectoryHref(sport = "") { 181 return `sports-directory.html${sportQuery(sport)}${sportDirectoryHash(sport)}`; 182} 183 184function newsCardPrimaryLabel(article = {}) { 185 const section = String(article.section || "").trim().toLowerCase(); 186 if (section.includes("daily market pulse")) return "Read what changed"; 187 if (section.includes("draft")) return "Read the angle"; 188 if (articleSignal(article)) return "Read the signal"; 189 if (section.includes("culture")) return "Read the argument"; 190 if (section.includes("history") || section.includes("evergreen")) return "Read the argument"; 191 return "Read the story"; 192} 193 194function newsCardHookText(article = {}) { 195 const text = cleanInlineText(article.dek || article.metaDescription || ""); 196 if (!text) return "The answer is up front. The consequence is the reason to keep reading."; 197 return text 198 .replace(/^(?:DME call|Desk read|Decision board|Lead question|Why the article works|Where skeptics should push)\s*:\s*/i, "") 199 .replace(/^[-*]\s+/, "") 200 .trim(); 201} 202 203function newsCardPayoffLine(article = {}) { 204 const sport = String(article.sport || "").trim().toUpperCase(); 205 const sportLabel = sport && sport !== "SPORTS" ? sport : "game"; 206 const section = String(article.section || "").toLowerCase(); 207 if (section.includes("history") || section.includes("culture") || section.includes("evergreen")) { 208 return "Start with the answer. Stay for the argument that keeps the moment alive."; 209 } 210 if (section.includes("daily market pulse")) { 211 return `Start with what changed. If it moves the read, open the ${sportLabel} board.`; 212 } 213 return `Start with the answer. Open the ${sportLabel} board only when the consequence matters.`; 214} 215 216const localApprovedArticleMedia = new Set([ 217 "2026-05-25-mlb-market-pulse", 218 "2026-05-26-mlb-market-pulse", 219 "2026-05-26-nba-market-pulse", 220 "2026-05-26-nfl-market-pulse", 221 "2026-05-26-nhl-market-pulse", 222 "2026-05-26-pga-market-pulse" 223]); 224
225const localArticleThumbnailOverrides = { 226 "1972-olympic-basketball-three-second-reset-market": "/assets/articles/approved/1972-olympic-basketball-three-second-reset-market-hero.png", 227 "2004-nba-finals-pistons-prince-angle-defense-market": "/assets/articles/approved/2004-nba-finals-pistons-prince-angle-defense-market-hero.png", 228 "sports-culture-bleacher-creatures-roll-call-belonging-market": "/assets/articles/approved/sports-culture-bleacher-creatures-roll-call-belonging-market-hero.png", 229 "2011-nba-finals-dallas-zone-miami-hesitation-market": "/assets/articles/approved/2011-nba-finals-dallas-zone-miami-hesitation-market-hero-clean.png", 230 "sports-culture-vuvuzela-sound-ritual-market": "/assets/articles/approved/sports-culture-vuvuzela-sound-ritual-market-hero-clean.png", 231 "derek-redmond-1992-arm-around-lane-six-market": "/assets/articles/approved/derek-redmond-1992-arm-around-lane-six-market-hero-clean.png", 232 "super-bowl-xlii-nascar-front-pressure-market": "/assets/articles/approved/super-bowl-xlii-nascar-front-pressure-market-hero.png", 233 "sports-culture-krzyzewskiville-tenting-belonging-market": "/assets/articles/approved/sports-culture-krzyzewskiville-tenting-belonging-market-hero.png", 234 "2001-world-series-game-7-rivera-bunt-error-market": "/assets/articles/approved/2001-world-series-game-7-rivera-bunt-error-market-hero.png", 235 "steve-bartman-foul-ball-scapegoat-market": "/assets/articles/approved/steve-bartman-foul-ball-scapegoat-market-hero.png", 236 "2019-nba-finals-raptors-box-and-one-curry-market": "/assets/articles/approved/2019-nba-finals-raptors-box-and-one-curry-market-hero.png", 237 "sports-culture-terrible-towel-civic-flag-market": "/assets/articles/approved/sports-culture-terrible-towel-civic-flag-market-hero.png", 238 "super-bowl-xliv-ambush-onside-kick-halftime-market": "/assets/articles/approved/super-bowl-xliv-ambush-onside-kick-halftime-market-hero.png", 239 "sports-culture-portland-victory-log-slab-belonging-market": "/assets/articles/approved/sports-culture-portland-victory-log-slab-belonging-market-hero.png" 240}; 241 242const localArticleCardThumbnailOverrides = { 243 "sports-culture-canucks-towel-power-belonging-market": "/assets/articles/approved/sports-culture-canucks-towel-power-belonging-market-card-clean.jpg", 244 "2009-champions-league-final-messi-false-nine-market": "/assets/articles/approved/2009-champions-league-final-messi-false-nine-market-card-clean.jpg", 245 "panenka-1976-penalty-chip-nerve-market": "/assets/articles/approved/panenka-1976-penalty-chip-nerve-market-card-clean.jpg", 246 "2026-06-02-ghana-partey-world-cup-squad-selection-storm": "/assets/articles/approved/2026-06-02-ghana-partey-world-cup-squad-selection-storm-card-clean.jpg", 247 "mankad-non-striker-runout-1947-cricket-law-market": "/assets/articles/approved/mankad-non-striker-runout-1947-cricket-law-market-card-v2.jpg", 248 "world-series-cricket-white-ball-tv-revolution-market": "/assets/articles/approved/world-series-cricket-white-ball-tv-revolution-market-thumb-v5.jpg" 249}; 250const NEWS_CARD_CLEAN_THUMB_VERSION = "20260606e"; 251 252function versionedNewsCardThumbnail(path = "") { 253 return `${path}?v=${NEWS_CARD_CLEAN_THUMB_VERSION}`; 254} 255
256function articleThumbnailUrl(article = {}) { 257 const slug = String(article.slug || "").trim(); 258 if (localArticleThumbnailOverrides[slug]) return localArticleThumbnailOverrides[slug]; 259 if (localApprovedArticleMedia.has(slug)) return `/assets/articles/approved/${slug}-01.png`; 260 const thumbnail = article.thumbnailUrl || ""; 261 return String(thumbnail).includes(".svg") ? "" : thumbnail; 262} 263 264function articleHologramFallbackUrl(article = {}) { 265 const haystack = [article.sport, article.section, article.title, article.slug].filter(Boolean).join(" ").toUpperCase(); 266 if (/(FIFA|WORLD CUP|SOCCER|UEFA|EPL|MLS|USMNT|USWNT|PREMIER LEAGUE)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-soccer.png"; 267 if (/(NASCAR|CUP SERIES|XFINITY)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nascar.png"; 268 if (/(PGA|GOLF|MASTERS|US OPEN|U\.S\. OPEN|OPEN CHAMPIONSHIP|RYDER CUP)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-pga.png"; 269 if (/(TENNIS|ATP|WTA|WIMBLEDON|ROLAND GARROS|US OPEN|AUSTRALIAN OPEN)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-tennis.png"; 270 if (/(CRICKET|IPL|MLC|ASHES|TEST CRICKET|T20|ODI|ICC|PLAYING XI|WHITE BALL|UNDERARM)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-cricket-generic-v1.png"; 271 if (/(NHL|HOCKEY|STANLEY CUP)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nhl.png"; 272 if (/(MLB|BASEBALL|WORLD SERIES)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-mlb.png"; 273 if (/(WNBA|NCAAWB)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-wnba.png"; 274 if (/(NBA|BASKETBALL|FINAL FOUR|MARCH MADNESS)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nba.png"; 275 if (/(NCAAF|COLLEGE FOOTBALL)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-ncaaf.png"; 276 if (/(NFL|FOOTBALL|SUPER BOWL)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nfl.png"; 277 return "/assets/articles/approved/dme-holo-logo-default.png"; 278} 279 280function newsCardCleanThumbnailUrl(thumbnail = "") { 281 const cleanPath = String(thumbnail || "").split("?")[0]; 282 if (!cleanPath.endsWith("-thumb-v2.jpg")) return ""; 283 return versionedNewsCardThumbnail(`${cleanPath.slice(0, -"-thumb-v2.jpg".length)}-card-clean.jpg`); 284} 285 286function newsThumbnailHasEmbeddedText(path = "") { 287 const cleanPath = String(path || "").split("?")[0].toLowerCase(); 288 if (cleanPath.includes("/assets/articles/approved/dme-ultra-raster-")) return false; 289 return cleanPath.includes("/assets/articles/approved/") || /-(thumb-v[0-9]+|card(?:-clean|-v[0-9]+))\.jpg$/.test(cleanPath); 290} 291 292function newsThumbnailUsesDbFallback(path = "") { 293 return String(path || "").split("?")[0].toLowerCase().includes("/assets/articles/approved/dme-ultra-raster-"); 294} 295 296function newsThumbnailUsesHologramLogo(path = "") { 297 return String(path || "").split("?")[0].toLowerCase().includes("/assets/articles/approved/dme-holo-logo-"); 298} 299 300function newsFallbackVisualAttributes(thumbnail = {}) { 301 if (!newsThumbnailUsesDbFallback(thumbnail.src)) return ""; 302 return ` data-visual-placement="news.card.fallback" data-visual-asset-slug="local-3d-visual-repository" data-visual-preserve-alt="true"`; 303} 304
305function articleThumbnailSource(article = {}) { 306 const src = articleThumbnailUrl(article); 307 if (!src) return { src: "", fallback: "", hasEmbeddedText: false }; 308 const slug = String(article.slug || "").trim(); 309 if (localArticleCardThumbnailOverrides[slug]) { 310 return { 311 src: versionedNewsCardThumbnail(localArticleCardThumbnailOverrides[slug]), 312 fallback: src, 313 hasEmbeddedText: true 314 }; 315 } 316 if (newsThumbnailUsesDbFallback(src)) { 317 return { 318 src: articleHologramFallbackUrl(article), 319 fallback: src, 320 hasEmbeddedText: false, 321 isHologramArt: true 322 }; 323 } 324 if (newsThumbnailUsesHologramLogo(src)) { 325 return { src, fallback: "", hasEmbeddedText: false, isHologramArt: true }; 326 } 327 const cleanSrc = newsCardCleanThumbnailUrl(src); 328 return cleanSrc 329 ? { src: cleanSrc, fallback: src, hasEmbeddedText: true } 330 : { src, fallback: "", hasEmbeddedText: newsThumbnailHasEmbeddedText(src), isHologramArt: false }; 331} 332 333function isPlayerSuperstarArticle(article = {}) { 334 return String(article.mediaTemplate || "").trim() === "player-superstar-feature" || 335 String(article.section || "").trim().toLowerCase() === "superstar fan guide"; 336} 337 338function articleOverlayLogoUrl(value = "") { 339 const url = String(value || "").trim(); 340 const cleanPath = url.split("?")[0].toLowerCase(); 341 if (cleanPath.includes("/teamlogos/soccer/500/364.png")) return "/assets/team-logos/clean/liverpool-fc.png"; 342 if (cleanPath.includes("/teamlogos/nhl/500/scoreboard/det.png") || cleanPath.includes("/teamlogos/nhl/500/det.png")) { 343 return "/assets/team-logos/clean/detroit-red-wings.png"; 344 } 345 return url; 346} 347 348function localSportLogoUrl(sport = "") { 349 const key = String(sport || "SPORTS").trim().toUpperCase() || "SPORTS"; 350 const identityLogo = window.DMLSportIdentity?.get?.(key)?.logoUrl; 351 if (identityLogo) return identityLogo; 352 const localLogos = { 353 CULTURE: "/assets/brand/sport-logos/dme-sport-culture-logo.png?v=20260704", 354 SPORTS: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704", 355 SPORT: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704", 356 ALL: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704", 357 NCAAMB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704", 358 NCAAB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704", 359 NCAAWB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704", 360 NCAAF: "/assets/brand/sport-logos/dme-sport-college-football-logo.svg?v=20260704-football6", 361 NCAA: "/assets/brand/sport-logos/dme-sport-college-logo.svg?v=20260704", 362 "COLLEGE SPORTS": "/assets/brand/sport-logos/dme-sport-college-logo.svg?v=20260704", 363 OLYMPICS: "/assets/brand/sport-logos/dme-sport-olympics-logo.svg?v=20260704", 364 OLYMPIC: "/assets/brand/sport-logos/dme-sport-olympics-logo.svg?v=20260704", 365 CYCLING: "/assets/brand/sport-logos/dme-sport-cycling-logo.svg?v=20260704", 366 RUNNING: "/assets/brand/sport-logos/dme-sport-running-logo.svg?v=20260704", 367 VOLLEYBALL: "/assets/brand/sport-logos/dme-sport-volleyball-logo.svg?v=20260704", 368 BOXING: "/assets/brand/sport-logos/dme-sport-boxing-logo.svg?v=20260704", 369 CRICKET: "/assets/brand/sport-logos/dme-sport-cricket-logo.svg?v=20260704", 370 RUGBY: "/assets/brand/sport-logos/dme-sport-rugby-logo.svg?v=20260704", 371 RACING: "/assets/brand/sport-logos/dme-sport-racing-logo.svg?v=20260704", 372 SOFTBALL: "/assets/brand/sport-logos/dme-sport-softball-logo.svg?v=20260704", 373 BASEBALL: "/assets/brand/sport-logos/dme-sport-baseball-logo.svg?v=20260704", 374 F1: "/assets/brand/sport-logos/dme-sport-f1-logo.svg?v=20260704", 375 UFC: "/assets/brand/sport-logos/dme-sport-combat-logo.svg?v=20260704", 376 MMA: "/assets/brand/sport-logos/dme-sport-combat-logo.svg?v=20260704", 377 "SPORTS MEDIA": "/assets/brand/sport-logos/dme-sport-media-logo.svg?v=20260704", 378 "NBA/NHL": "/assets/brand/sport-logos/dme-sport-multi-league-logo.svg?v=20260704" 379 }; 380 return localLogos[key] || localLogos.SPORTS; 381} 382 383function renderNewsTeamLogoBadge(article = {}, options = {}) { 384 if (options.authoredArt) return "";
385 const logos = Array.isArray(article.teamLogos) ? article.teamLogos.filter((item) => item?.logoUrl) : []; 386 const sport = String(article.sport || "SPORTS").trim().toUpperCase() || "SPORTS"; 387 const authoredClass = options.authoredArt ? " is-authored-art" : ""; 388 const hologramClass = options.hologramBack ? " is-hologram-back" : ""; 389 if (!logos.length) { 390 if (options.suppressLeagueFallback) return ""; 391 const localLogo = localSportLogoUrl(sport); 392 return ` 393 <div class="news-card-team-logos is-league-logo${authoredClass}${hologramClass}" aria-label="${escapeHtml(sport)} league mark" title="${escapeHtml(sport)} league mark"> 394 <span class="news-card-team-logo" data-sport-logo-target="${escapeHtml(sport)}">${ 395 localLogo ? `<img src="${escapeHtml(localLogo)}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" />` : "" 396 }</span> 397 </div> 398 `; 399 } 400 if (!logos.length) return ""; 401 const label = logos.map((item) => item.teamName || item.abbreviation || item.sport).filter(
401Boolean).join(" / "); 402 return ` 403 <div class="news-card-team-logos${authoredClass}${hologramClass}" aria-label="Team context: ${escapeHtml(label)}" title="${escapeHtml(label)}"> 404 ${logos 405 .slice(0, 3) 406 .map( 407 (item) => ` 408 <span class="news-card-team-logo"> 409 <img src="${escapeHtml(articleOverlayLogoUrl(item.logoUrl))}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" /> 410 </span> 411 ` 412 ) 413 .join("")} 414 </div> 415 `; 416} 417 418function headlineOverlayText(article = {}) { 419 const title = String(article.title || "").trim(); 420 if (!title) return "Market read"; 421 const split = title.split(/[:|]/)[0]?.trim() || title; 422 return split.length > 74 ? `${split.slice(0, 71).trim()}...` : split; 423} 424 425function renderNewsMediaOverlay(article = {}, signal = null) { 426 const sport = String(article.sport || "Sports").trim().toUpperCase(); 427 const section = String(article.section || "Market Pulse").trim(); 428 const label = signal?.label || section; 429 return ` 430 <div class="news-card-media-overlay" aria-hidden="true"> 431 <span>${escapeHtml(sport)} / ${escapeHtml(label)}</span> 432 <strong>${escapeHtml(headlineOverlayText(article))}</strong> 433 </div> 434 `; 435} 436 437const state = { 438 articles: [], 439 readerFilter: "all", 440 query: cleanInlineText(new URLSearchParams(location.search).get("q") || "").slice(0, 120), 441 sort: activeArticleSort(), 442 year: activeArticleYear(), 443 month: activeArticleMonth(), 444 counts: {}, 445 pagination: null, 446 facets: { years: [] }, 447 page: 1, 448 pageSize: 12, 449 totalPages: 1 450}; 451let newsRequestId = 0; 452let searchDebounceTimer = 0; 453 454function articleLibrary(article = {}) { 455 const section = String(article.section || "").toLowerCase(); 456 const sport = String(article.sport || "").toUpperCase(); 457 if (section.includes("draft lab")) return "draft"; 458 if (section === "sports culture" || sport === "CULTURE") return "culture"; 459 if (section.includes("history") || section.includes("evergreen")) return "history"; 460 if (section === "trending" || section === "daily market pulse" || section === "breaking market pulse" || section === "world cup 26 preview") return "daily"; 461 if (section === "fantasy waiver brief") return "archive"; 462 return "other"; 463} 464 465function articleLaneKey(article = {}) { 466 const lane = laneFilterFor(article.lane || article.laneRoutePrefix || ""); 467 if (lane) return lane.routePrefix; 468 const path = cleanInlineText(article.canonicalPath || ""); 469 const [, routePrefix] = path.match(/^\/([^/]+)\//) || []; 470 return laneFilterFor(routePrefix)?.routePrefix || ""; 471} 472 473function articleBelongsToCurrentView(article = {}) { 474 const type = activeType(); 475 const sport = activeSport(); 476 const library = articleLibrary(article); 477 const signal = activeSignal(); 478 const section = activeSection(); 479 const lane = activeLane(); 480 if (section && normalizeSearchText(article.section || "") !== normalizeSearchText(section)) return false; 481 if (lane && articleLaneKey(article) !== lane) return false; 482 if (signal) return library === "daily"; 483 if (type === "draft") return library === "draft"; 484 if (lane) return library === "history" || library === "culture"; 485 if (type === "evergreen" && sport === "CULTURE") return library === "culture"; 486 if (type === "evergreen") return library === "history"; 487 if (type === "daily") return library === "daily"; 488 if (type === "archive") return library === "archive"; 489 if (sport && sport !== "CULTURE") return String(article.sport || "").toUpperCase() === sport; 490 return true; 491} 492 493function hoursSince(value) { 494 const date = new Date(value || Date.now()); 495 if (Number.isNaN(date.getTime())) return 999; 496 return Math.max(0, (Date.now() - date.getTime()) / 36e5); 497} 498 499function articleTimestamp(article = {}) { 500 const time = new Date(article.publishedAt || article.createdAt || 0).getTime(); 501 return Number.isFinite(time) ? time : 0; 502} 503 504function articleYear(article = {}) { 505 const time = articleTimestamp(article); 506 return time ? String(new Date(time).getFullYear()) : ""; 507} 508 509function articleMonth(article = {}) { 510 const time = articleTimestamp(article); 511 return time ? String(new Date(time).getMonth() + 1).padStart(2, "0") : ""; 512} 513 514function articleMatchesYear(article = {}) { 515 return !state.year || articleYear(article) === state.year; 516} 517 518function articleMatchesMonth(article = {}) { 519 return !state.month || articleMonth(article) === state.month; 520} 521 522function articleSignalText(article = {}) { 523 return [article.title, article.dek, article.section, article.sport, article.metaDescription].join(" ").toLowerCase(); 524} 525 526function normalizeSearchText(value) { 527 return String(value ?? "") 528 .normalize("NFKD") 529 .replace(/[\u0300-\u036f]/g, "") 530 .replace(/[ââ]/g, "'") 531 .replace(/[ââ]/g, '"') 532 .replace(/[\u2010-\u2015]/g, "-") 533 .toLowerCase() 534 .replace(/[^a-z0-9]+/g, " ") 535 .trim(); 536} 537 538function articleSearchText(article = {}) { 539 return normalizeSearchText([ 540 article.title, 541 article.seoTitle, 542 article.dek, 543 article.metaDescription, 544 article.section, 545 article.sport, 546 article.author, 547 article.slug, 548 article.canonicalPath, 549 article.imageAlt, 550 article.mediaCaption, 551 article.visualTheme, 552 article.visualPrompt, 553 article.lane, 554 article.laneRoutePrefix 555 ].join(" ")); 556} 557 558function articleMatchesSearch(article = {}) { 559 const query = normalizeSearchText(state.query); 560 if (!query) return true; 561 if (state.counts.searchTotal !== null && state.counts.searchTotal !== undefined) return true;
562 const terms = query.split(/\s+/).filter(Boolean); 563 const haystack = articleSearchText(article); 564 return terms.every((term) => haystack.includes(term)); 565} 566 567function breakingScore(article = {}) { 568 const text = articleSignalText(article); 569 const hours = hoursSince(article.publishedAt || article.createdAt); 570 let score = Math.max(0, 72 - hours * 2.4); 571 if (/breaking|injury|injured|out|questionable|scratch|scratched|trade|signed|waived|returns|lineup|starter|pitcher|goalie|weather|delay|final|wins|leads|playoff/i.test(text)) score += 18; 572 if (String(article.section || "").toLowerCase().includes("breaking")) score += 24; 573 if (Number(article.wordCount || 0) >= 900) score += 4; 574 return Math.round(Math.min(100, score)); 575} 576 577function trendingScore(article = {}) { 578 const text = articleSignalText(article); 579 const hours = hoursSince(article.publishedAt || article.createdAt); 580 let score = Math.max(0, 58 - hours * 0.85); 581 if (/viral|trend|trending|buzz|debate|reaction|sweep|\bfinal\b|finals|championship|game\s*7|draft|mock|waiver|injury|odds|fantasy|playoff|trade|rumor/i.test(text)) score += 18; 582 if (Number(article.wordCount || 0) >= 1200) score += 8; 583 if (Number(article.wordCount || 0) >= 1800) score += 5; 584 if (article.isPremium) score += 3; 585 return Math.round(Math.min(100, score)); 586} 587 588function articleSignal(article = {}) { 589 const signal = activeSignal(); 590 if (signal === "breaking") return { label: "Breaking Watch", score: breakingScore(article) }; 591 if (signal === "trending") return { label: "Trending Score", score: trendingScore(article) }; 592 const breakScore = breakingScore(article); 593 const trendScore = trendingScore(article); 594 if (breakScore >= 72) return { label: "Breaking Watch", score: breakScore }; 595 if (trendScore >= 72) return { label: "Trending", score: trendScore }; 596 return null; 597} 598 599const SOURCE_CONFIDENCE_BADGES = { 600 schedule: { label: "Schedule verified", className: "schedule" }, 601 source: { label: "Source depth", className: "source" }, 602 injury: { label: "Injury watch", className: "injury" }, 603 weather: { label: "Weather risk", className: "weather" }, 604 market: { label: "Market move", className: "market" }, 605 lineup: { label: "Lineup check", className: "lineup" } 606}; 607 608function articleConfidenceBadges(article = {}, signal = null) { 609 const text = articleSignalText(article); 610 const keys = ["schedule"]; 611 if (/market|odds|line move|line movement|spread|total|price|pricing|bookmaker|ticket|resale|transfer/.test(text) || signal) keys.push("market"); 612 if (/injury|injured|out|questionable|scratch|availability|hamstring|illness|suspended/.test(text)) keys.push("injury"); 613 if (/weather|wind|rain|delay|roof|venue|heat|storm|field condition/.test(text)) keys.push("weather"); 614 if (/lineup|starter|starting|pitcher|goalie|rotation|minutes|usage|roster|depth chart/.test(text)) keys.push("lineup"); 615 if (Number(article.wordCount || 0) >= 900 || article.thumbnailUrl || article.mediaAssets?.length) keys.push("source"); 616 if (!keys.includes("source")) keys.push("source"); 617 return [...new Set(keys)] 618 .slice(0, 3) 619 .map((key) => SOURCE_CONFIDENCE_BADGES[key]) 620 .filter(Boolean); 621} 622 623function renderArticleConfidenceBadges(article = {}, signal = null) { 624 const badges = articleConfidenceBadges(article, signal); 625 return ` 626 <div class="article-proof-badges" aria-label="Article source confidence tags"> 627 ${badges 628 .map( 629 (badge) => ` 630 <span class="article-proof-badge is-${badge.className}"> 631 <i aria-hidden="true"></i> 632 <b>${escapeHtml(badge.label)}</b> 633 </span> 634 ` 635 ) 636 .join("")} 637 </div> 638 `; 639} 640 641function isAutomatedNewsArticle(article = {}) { 642 const section = String(article.section || ""); 643 return Boolean(article.isAutoGenerated || /Market Pulse|World Cup 26 Preview/i.test(section)); 644} 645 646function newsUpdateStatus(article = {}, signal = null) { 647 const publishedAt = article.publishedAt || article.createdAt; 648 const hours = hoursSince(publishedAt); 649 const fresh = hours <= 24; 650 const automated = isAutomatedNewsArticle(article); 651 if (signal?.label && fresh) { 652 if (/trend/i.test(signal.label)) return "Trend alert updated"; 653 return "News alert updated"; 654 } 655 if (automated) return ""; 656 if (fresh) return "Fresh news update"; 657 return ""; 658} 659 660function articleMatchesSignal(article = {}) { 661 const signal = activeSignal(); 662 // Breaking is a ranked view of the current news feed, not an absolute gate. 663 // The freshness component naturally decays, so a score cutoff can make the 664 // homepage's newsroom link hide every published article between updates. 665 if (signal === "breaking") return true; 666 // Trending is a ranked view of the current news feed, not an absolute gate. 667 // An absolute score cutoff can hide every article as the feed ages, leaving 668 // the homepage's primary CTA pointed at an empty newsroom. 669 if (signal === "trending") return true; 670 return true; 671} 672 673function articleMatches(article) { 674 const readerOk = 675 state.readerFilter === "all" || 676 (state.readerFilter === "premium" && article.isPremium) || 677 (state.readerFilter === "free" && !article.isPremium); 678 return articleBelongsToCurrentView(article) && articleMatchesSignal(article) && readerOk && articleMatchesSearch(article) && articleMatchesYear(article) && articleMatchesMonth(article); 679} 680
681function sortArticlesForView(articles = []) { 682 const signal = activeSignal(); 683 const list = [...articles]; 684 if (signal === "breaking" && state.sort !== "oldest") return list.sort((a, b) => breakingScore(b) - breakingScore(a) || articleTimestamp(b) - articleTimestamp(a)); 685 if (signal === "trending" && state.sort !== "oldest") return list.sort((a, b) => trendingScore(b) - trendingScore(a) || articleTimestamp(b) - articleTimestamp(a)); 686 const direction = state.sort === "oldest" ? 1 : -1; 687 return list.sort((a, b) => { 688 const timeDiff = articleTimestamp(a) - articleTimestamp(b); 689 if (timeDiff !== 0) return timeDiff * direction; 690 return (Number(a.id || 0) - Number(b.id || 0)) * direction; 691 }); 692} 693 694function availableArticleYears(articles = []) { 695 const serverYears = Array.isArray(state.facets?.years) ? state.facets.years.map(String).filter(Boolean) : []; 696 if (serverYears.length) { 697 if (state.year && !serverYears.includes(state.year)) serverYears.push(state.year); 698 return [...new Set(serverYears)].sort((a, b) => Number(b) - Number(a)); 699 } 700 const years = new Set(); 701 articles.forEach((article) => { 702 if (!articleBelongsToCurrentView(article)) return; 703 const year = articleYear(article); 704 if (year) years.add(year); 705 }); 706 if (state.year) years.add(state.year); 707 return Array.from(years).sort((a, b) => Number(b) - Number(a)); 708} 709 710function availableArticleMonths(articles = []) { 711 return ARTICLE_MONTHS; 712} 713 714function renderArticleControls(articles = []) { 715 document.querySelectorAll("[data-sort-control]").forEach((button) => { 716 const isActive = normalizeArticleSort(button.dataset.sortControl || "") === state.sort; 717 button.classList.toggle("active", isActive); 718 button.setAttribute("aria-pressed", String(isActive)); 719 }); 720 const yearSelect = document.querySelector("#articleYear"); 721 if (!yearSelect) return; 722 const years = availableArticleYears(articles); 723 const signature = years.join("|"); 724 if (yearSelect.dataset.yearSignature !== signature) { 725 yearSelect.innerHTML = [ 726 '<option value="">All years</option>', 727 ...years.map((year) => `<option value="${escapeHtml(year)}">${escapeHtml(year)}</option>`) 728 ].join(""); 729 yearSelect.dataset.yearSignature = signature; 730 } 731 yearSelect.value = state.year; 732 const monthSelect = document.querySelector("#articleMonth"); 733 if (!monthSelect) return; 734 const months = availableArticleMonths(articles); 735 const monthSignature = months.map((month) => month.value).join("|"); 736 if (monthSelect.dataset.monthSignature !== monthSignature) { 737 monthSelect.innerHTML = [ 738 '<option value="">All months</option>', 739 ...months.map((month) => `<option value="${escapeHtml(month.value)}">${escapeHtml(month.label)}</option>`) 740 ].join(""); 741 monthSelect.dataset.monthSignature = monthSignature; 742 } 743 monthSelect.value = state.month; 744} 745 746function renderPagination(totalItems = 0) { 747 const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize)); 748 state.totalPages = totalPages; 749 state.page = Math.min(Math.max(1, state.page), totalPages); 750 const start = totalItems ? (state.page - 1) * state.pageSize + 1 : 0; 751 const end = Math.min(totalItems, state.page * state.pageSize); 752 const controls = document.querySelectorAll(".article-pagination"); 753 const shouldDisablePrev = state.page <= 1; 754 const shouldDisableNext = state.page >= totalPages; 755 const markup = ` 756 <button type="button" data-page-action="first" ${shouldDisablePrev ? "disabled" : ""}>First</button> 757 <button type="button" data-page-action="prev" ${shouldDisablePrev ? "disabled" : ""}>Prev</button> 758 <span>Page ${state.page} of ${totalPages} | ${start}-${end} of ${totalItems}</span> 759 <button type="button" data-page-action="next" ${shouldDisableNext ? "disabled" : ""}>Next</button> 760 <button type="button" data-page-action="last" ${shouldDisableNext ? "disabled" : ""}>Last</button> 761 `; 762 controls.forEach((control) => { 763 control.innerHTML = totalItems > state.pageSize ? markup : totalItems && control.classList.contains("bottom") ? `<span>${totalItems} articles</span>` : ""; 764 }); 765} 766 767function renderArticles(articles, errorMessage = "") { 768 const grid = document.querySelector("#newsGrid"); 769 const count = document.querySelector("#articleCount"); 770 renderArticleControls(articles); 771 if (!articles.length) { 772 if (count) count.textContent = errorMessage ? "Newsroom feed temporarily unavailable" : `No ${libraryLabel()} articles loaded yet`; 773 renderPagination(0); 774 grid.innerHTML = ` 775 <article class="news-card empty-state">
776 <span>${errorMessage ? "Newsroom Status" : "Article Library"}</span> 777 <h3>${errorMessage ? "The newsroom feed is temporarily unavailable." : `No ${libraryLabel()} articles are available for this view yet.`}</h3> 778 <p>${ 779 errorMessage 780 ? "The page is online, but the article feed did not load. Please check back shortly." 781 : "New market pulse, history, and culture articles will appear here as soon as they are published." 782 }</p> 783 <a class="action-button" href="news.html?type=evergreen">Open History Library</a> 784 </article> 785 `; 786 return; 787 } 788 const scopedArticles = articles.filter(articleBelongsToCurrentView); 789 const visibleArticles = sortArticlesForView(articles.filter(articleMatches)); 790 const serverPaginated = Boolean(state.pagination && Number.isFinite(Number(state.pagination.total))); 791 const totalItems = serverPaginated ? Number(state.pagination.total) : visibleArticles.length; 792 const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize)); 793 state.page = serverPaginated 794 ? Number(state.pagination.page || state.page) 795 : Math.min(Math.max(1, state.page), totalPages); 796 const pageStart = serverPaginated ? Number(state.pagination.offset || 0) : (state.page - 1) * state.pageSize; 797 const pageArticles = serverPaginated ? visibleArticles : visibleArticles.slice(pageStart, pageStart + state.pageSize); 798 renderPagination(totalItems); 799 if (count) { 800 const viewLabel = currentViewLabel(); 801 const viewTotal = Number(state.counts.viewTotal || scopedArticles.length || 0); 802 const databaseTotal = Number(state.counts.databaseTotal || state.articles.length || 0); 803 const searchActive = Boolean(normalizeSearchText(state.query)); 804 const searchTotalValue = Number(state.counts.searchTotal); 805 const hasSearchTotal = searchActive && Number.isFinite(searchTotalValue); 806 const totalLabel = viewTotal === databaseTotal ? `${viewTotal} in DB` : `${viewTotal} in ${viewLabel} | ${databaseTotal} total DB`; 807 const shownStart = totalItems ? pageStart + 1 : 0; 808 const shownEnd = Math.min(totalItems, pageStart + pageArticles.length); 809 const shownLabel = totalItems > state.pageSize ? `${shownStart}-${shownEnd} shown` : `${totalItems} shown`; 810 if (hasSearchTotal) { 811 const matchNoun = searchTotalValue === 1 ? "match" : "matches"; 812 count.textContent = `${shownLabel} | ${searchTotalValue} DB ${matchNoun} | ${databaseTotal} total DB`; 813 } else { 814 const loadedLabel = serverPaginated ? "" : state.readerFilter !== "all" || state.query || viewTotal > scopedArticles.length ? ` | ${scopedArticles.length} loaded` : ""; 815 count.textContent = `${shownLabel}${loadedLabel} | ${totalLabel}`; 816 } 817 } 818 if (!visibleArticles.length) { 819 renderPagination(0); 820 const hasSearch = Boolean(normalizeSearchText(state.query)); 821 const searchTotalValue = Number(state.counts.searchTotal); 822 const searchContext = hasSearch && Number.isFinite(searchTotalValue) 823 ? `Searched the full ${Number(state.counts.databaseTotal || 0).toLocaleString()}-article database for "${escapeHtml(state.query)}".` 824 : "Clear the search or switch the reader-value filter to see the full library."; 825 grid.innerHTML = ` 826 <article class="news-card empty-state"> 827 <span>Filter Check</span> 828 <h3>${hasSearch ? "No database matches for that search yet." : "No articles match that filter yet."}</h3> 829 <p>${searchContext}</p> 830 <div class="news-empty-actions"> 831 ${hasSearch ? `<button type="button" class="action-button" data-clear-search>Clear Search</button>` : `<a class="action-button" href="news.html?type=evergreen">Open History Library</a>`} 832 <a class="action-button secondary-action" href="news.html">All Articles</a> 833 <a class="action-button secondary-action" href="news.html?type=evergreen">History</a> 834 </div> 835 </article> 836 `; 837 bindClearSearch(); 838 return; 839 } 840 grid.innerHTML = pageArticles 841 .map( 842 (article) => { 843 const href = article.canonicalPath || `/news/${article.slug}`;
844 const thumbnail = articleThumbnailSource(article); 845 const signal = articleSignal(article); 846 const updateStatus = newsUpdateStatus(article, signal); 847 const sport = String(article.sport || "").trim().toUpperCase(); 848 const sportParam = sportQuery(sport); 849 const mediaClasses = [ 850 "news-card-media", 851 thumbnail.hasEmbeddedText ? "has-authored-art" : "", 852 thumbnail.isHologramArt ? "has-hologram-art" : "", 853 isPlayerSuperstarArticle(article) ? "has-player-portrait" : "" 854 ].filter(Boolean).join(" "); 855 return ` 856 <article class="news-card" data-article-href="${escapeHtml(href)}" data-article-title="${escapeHtml(article.title)}" tabindex="0" role="link" aria-label="Open article: ${escapeHtml(article.title)}"> 857 ${ 858 thumbnail.src 859 ? `<div class="${mediaClasses}"> 860 <img src="${escapeHtml(thumbnail.src)}" ${ 861 thumbnail.fallback ? `data-fallback-src="${escapeHtml(thumbnail.fallback)}"` : "" 862 }${newsFallbackVisualAttributes(thumbnail)} alt="${escapeHtml(article.imageAlt || article.title)}" loading="lazy" decoding="async" width="640" height="360" /> 863 ${thumbnail.hasEmbeddedText || thumbnail.isHologramArt ? "" : renderNewsMediaOverlay(article, signal)} 864 ${renderNewsTeamLogoBadge(article, { authoredArt: thumbnail.hasEmbeddedText, hologramBack: thumbnail.isHologramArt, suppressLeagueFallback: thumbnail.isHologramArt })} 865 </div>` 866 : "" 867 } 868 <div class="news-card-body"> 869 <span class="news-card-kicker">${escapeHtml(article.sport || "Sports")} | ${escapeHtml(formatPostedAt(article.publishedAt || article.createdAt))}</span> 870 <div class="news-card-alert-row"> 871 ${updateStatus ? `<strong class="article-update-badge">${escapeHtml(updateStatus)}</strong>` : ""} 872 ${signal ? `<strong class="article-signal-badge">${escapeHtml(signal.label)} ${Number(signal.score || 0)}</strong>` : ""} 873 </div> 874 ${renderArticleConfidenceBadges(article, signal)} 875 <h3>${escapeHtml(article.title)}</h3> 876 <p>${escapeHtml(newsCardHookText(article))}</p> 877 <p class="news-card-payoff">${escapeHtml(newsCardPayoffLine(article))}</p> 878 <div class="news-card-meta"> 879 <b>${escapeHtml(article.section)}</b> 880 <b>${Number(article.wordCount || 0).toLocaleString()} words</b> 881 </div> 882 <div class="card-action-row"> 883 <a class="action-button" href="${escapeHtml(href)}" data-key-event="article_read_start" data-key-target="${escapeHtml(article.slug || article.title || href)}">${escapeHtml(newsCardPrimaryLabel(article))}</a> 884 <a class="action-button secondary-action" href="picks.html${sportParam}" data-key-event="game_brief_open" data-key-target="news-card-brief-${escapeHtml(article.slug || sport || "all")}">Open board</a> 885 <a class="action-button secondary-action" href="${escapeHtml(sportDirectoryHref(sport))}" data-key-event="directory_open" data-key-target="news-card-directory-${escapeHtml(article.slug || sport || "all")}">Sport hub</a> 886 </div> 887 </div> 888 </article> 889 `; 890 } 891 ) 892 .join(""); 893 bindImageFallbacks(); 894 bindArticleCards(); 895 window.DMLVisualAssets?.applyAll?.();
896 window.DMLSportIdentity?.applyAll?.(); 897 window.DMLSportIdentity?.ready?.then(() => window.DMLSportIdentity?.applyAll?.()).catch(() => {}); 898} 899 900function bindClearSearch() { 901 document.querySelectorAll("[data-clear-search]").forEach((button) => { 902 button.addEventListener("click", () => { 903 const search = document.querySelector("#articleSearch"); 904 state.query = ""; 905 state.page = 1; 906 if (search) search.value = ""; 907 syncNewsControlUrl(); 908 loadNews(); 909 search?.focus(); 910 }); 911 }); 912} 913 914function bindImageFallbacks() { 915 document.querySelectorAll("img[data-fallback-src]").forEach((image) => { 916 image.addEventListener("error", () => { 917 if (image.dataset.fallbackUsed === "true") return; 918 const fallback = image.dataset.fallbackSrc; 919 if (!fallback) return; 920 image.dataset.fallbackUsed = "true"; 921 image.src = fallback; 922 }); 923 }); 924} 925 926function openArticleCard(href, event) { 927 if (!href) return; 928 if (typeof window.trackDmlKeyEvent === "function") { 929 const card = event?.currentTarget || event?.target?.closest?.("[data-article-href]"); 930 window.trackDmlKeyEvent("article_read_start", card?.dataset?.articleTitle || href, { 931 href, 932 text: card?.dataset?.articleTitle || "Article card", 933 source: "news-card" 934 }); 935 } 936 if (event?.metaKey || event?.ctrlKey) { 937 window.open(href, "_blank", "noopener"); 938 return; 939 } 940 window.location.href = href; 941} 942 943function bindArticleCards() { 944 document.querySelectorAll(".news-card[data-article-href]").forEach((card) => { 945 card.addEventListener("click", (event) => { 946 if (event.target.closest("a, button, input, textarea, select, label")) return; 947 openArticleCard(card.dataset.articleHref, event); 948 }); 949 card.addEventListener("keydown", (event) => { 950 if (event.target.closest("a, button, input, textarea, select, label") && event.target !== card) return; 951 if (event.key !== "Enter" && event.key !== " ") return; 952 event.preventDefault(); 953 openArticleCard(card.dataset.articleHref, event); 954 }); 955 }); 956} 957 958function setSearchFromUrl(url = new URL(location.href)) { 959 state.query = cleanInlineText(url.searchParams.get("q") || "").slice(0, 120); 960 state.sort = activeArticleSort(url); 961 state.year = activeArticleYear(url); 962 state.month = activeArticleMonth(url); 963 const search = document.querySelector("#articleSearch"); 964 if (search) search.value = state.query; 965 renderArticleControls(state.articles); 966} 967 968function syncNewsControlUrl({ replace = true } = {}) { 969 const url = new URL(location.href); 970 if (state.query.trim()) url.searchParams.set("q", state.query.trim()); 971 else url.searchParams.delete("q"); 972 if (state.sort === "oldest") url.searchParams.set("sort", "oldest"); 973 else url.searchParams.delete("sort"); 974 if (state.year) url.searchParams.set("year", state.year); 975 else url.searchParams.delete("year"); 976 if (state.month) url.searchParams.set("month", state.month); 977 else url.searchParams.delete("month"); 978 const next = `${url.pathname}${url.search}${url.hash}`; 979 if (next === `${location.pathname}${location.search}${location.hash}`) return; 980 const method = replace ? "replaceState" : "pushState"; 981 history[method]({ newsFilter: true }, "", next); 982} 983 984function scrollNewsResultsIntoView() { 985 const panel = document.querySelector(".news-results-panel"); 986 if (!panel) return; 987 const toolbar = document.querySelector(".site-toolbar"); 988 const toolbarBottom = Math.max(0, toolbar?.getBoundingClientRect?.().bottom || 0); 989 const top = Math.max(0, window.scrollY + panel.getBoundingClientRect().top - toolbarBottom - 12); 990 window.scrollTo({ top, behavior: "smooth" }); 991} 992 993function bindNewsTabLinks() { 994 document.querySelectorAll(".news-tabs a").forEach((link) => { 995 link.addEventListener("click", (event) => { 996 if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return; 997 const target = new URL(link.href, location.href); 998 if (target.origin !== location.origin || target.pathname !== location.pathname) return; 999 event.preventDefault(); 1000 if (state.sort === "oldest") target.searchParams.set("sort", "oldest"); 1001 if (state.year) target.searchParams.set("year", state.year); 1002 if (state.month) target.searchParams.set("month", state.month); 1003 history.pushState({ newsFilter: true }, "", `${target.pathname}${target.search}${target.hash}`); 1004 setSearchFromUrl(target); 1005 state.page = 1; 1006 loadNews(); 1007 }); 1008 }); 1009 window.addEventListener("popstate", () => { 1010 setSearchFromUrl(); 1011 state.page = 1; 1012 loadNews(); 1013 }); 1014} 1015 1016async function loadNews() { 1017 const requestId = ++newsRequestId; 1018 const sport = activeSport(); 1019 const type = activeType(); 1020 const signal = activeSignal(); 1021 const section = activeSection(); 1022 const lane = activeLane(); 1023 const laneMeta = activeLaneMeta(); 1024 const title = document.querySelector("#newsTitle"); 1025 if (section) title.textContent = sport ? `${sport} ${section} Articles` : `${section} Articles`; 1026 else if (signal === "breaking") title.textContent = sport ? `${sport} Breaking News` : "Breaking News"; 1027 else if (signal === "trending") title.textContent = sport ? `${sport} Trending Articles` : "Trending Articles"; 1028 else if (laneMeta) title.textContent = `${laneMeta.label} Articles`; 1029 else if (type === "draft") title.textContent = sport ? `${sport} Draft Lab` : "Draft Lab"; 1030 else if (type === "evergreen" && sport === "CULTURE") title.textContent = "Sports Culture Articles"; 1031 else if (type === "evergreen") title.textContent = sport ? `${sport} Historical Features` : "Historical Features"; 1032 else if (type === "daily") title.textContent = sport ? `${sport} Market Pulse Articles` : "Latest Market Pulse Articles"; 1033 else if (type === "archive") title.textContent = sport ? `${sport} Waiver Receipt Archive` : "Waiver Receipt Archive"; 1034 else title.textContent = sport ? `All ${sport} Articles` : "All Articles"; 1035 let activeNewsTab = null; 1036 document.querySelectorAll(".news-tabs a").forEach((link) => { 1037 const linkSport = new URL(link.href, location.href).searchParams.get("sport")?.toUpperCase() || ""; 1038 const rawType = new URL(link.href, location.href).searchParams.get("type"); 1039 const linkType = rawType === "draft" ? "draft" : rawType === "evergreen" ? "evergreen" : rawType === "daily" ? "daily" : rawType === "archive" || rawType === "waiver" ? "archive" : "all"; 1040 const linkSignal = new URL(link.href, location.href).searchParams.get("signal") || ""; 1041 const linkSection = cleanInlineText(new URL(link.href, location.href).searchParams.get("section") || "").slice(0, 80); 1042 const linkLane = laneFilterFor(new URL(link.href, location.href).searchParams.get("lane") || "")?.routePrefix || ""; 1043 const isActive = linkSport === sport && linkType === type && linkSignal === signal && linkSection === section && linkLane === lane; 1044 link.classList.toggle("active", isActive); 1045 if (isActive) link.setAttribute("aria-current", "page"); 1046 else link.removeAttribute("aria-current"); 1047 if (isActive) activeNewsTab = link; 1048 });
1049 const activeGroup = activeNewsTab?.closest(".news-tab-group"); 1050 if (activeGroup && activeGroup.scrollWidth > activeGroup.clientWidth) { 1051 activeGroup.scrollLeft = Math.max(0, activeNewsTab.offsetLeft - activeGroup.clientWidth / 2 + activeNewsTab.offsetWidth / 2); 1052 } 1053 try { 1054 const params = new URLSearchParams(); 1055 const query = state.query.trim(); 1056 if (sport) params.set("sport", sport); 1057 if (type === "evergreen" || type === "draft" || type === "daily" || type === "archive") params.set("type", type); 1058 if (section) params.set("section", section); 1059 if (lane) params.set("lane", lane); 1060 if (state.sort === "oldest") params.set("sort", "oldest"); 1061 if (state.year) params.set("year", state.year); 1062 if (state.month) params.set("month", state.month); 1063 if (query) params.set("q", query); 1064 if (state.readerFilter !== "all") params.set("reader", state.readerFilter); 1065 if (signal) { 1066 params.set("limit", "60"); 1067 } else { 1068 params.set("page", String(state.page)); 1069 params.set("pageSize", String(state.pageSize)); 1070 } 1071 const response = await fetch(`/api/news${params.toString() ? `?${params}` : ""}`); 1072 if (!response.ok) throw new Error("News failed"); 1073 const { articles, counts, pagination, facets } = await response.json(); 1074 if (requestId !== newsRequestId) return; 1075 state.articles = articles || []; 1076 state.counts = counts || {}; 1077 state.pagination = pagination || null; 1078 state.facets = facets || { years: [] }; 1079 renderArticles(state.articles); 1080 } catch { 1081 if (requestId !== newsRequestId) return; 1082 state.pagination = null; 1083 state.facets = { years: [] }; 1084 renderArticles([], "api-error"); 1085 } 1086} 1087 1088function bindFilters() { 1089 const search = document.querySelector("#articleSearch"); 1090 if (search && state.query) search.value = state.query; 1091 search?.addEventListener("input", () => { 1092 state.query = search.value.trim(); 1093 state.page = 1; 1094 const count = document.querySelector("#articleCount"); 1095 if (count) count.textContent = state.query ? "Searching full article database..." : "Loading articles"; 1096 window.clearTimeout(searchDebounceTimer); 1097 searchDebounceTimer = window.setTimeout(() => { 1098 syncNewsControlUrl(); 1099 loadNews(); 1100 }, state.query ? 220 : 0); 1101 }); 1102 document.querySelectorAll("[data-sort-control]").forEach((button) => { 1103 button.addEventListener("click", () => { 1104 const nextSort = normalizeArticleSort(button.dataset.sortControl || ""); 1105 if (state.sort === nextSort) return; 1106 state.sort = nextSort; 1107 state.page = 1; 1108 renderArticleControls(state.articles); 1109 const count = document.querySelector("#articleCount"); 1110 if (count) count.textContent = nextSort === "oldest" ? "Loading archive floor..." : "Loading latest articles..."; 1111 syncNewsControlUrl(); 1112 loadNews(); 1113 }); 1114 }); 1115 const yearSelect = document.querySelector("#articleYear"); 1116 yearSelect?.addEventListener("change", () => { 1117 state.year = normalizeArticleYear(yearSelect.value); 1118 state.page = 1; 1119 renderArticleControls(state.articles); 1120 const count = document.querySelector("#articleCount"); 1121 if (count) count.textContent = state.year ? `Loading ${state.year} articles...` : "Loading all years..."; 1122 syncNewsControlUrl(); 1123 loadNews(); 1124 }); 1125 const monthSelect = document.querySelector("#articleMonth"); 1126 monthSelect?.addEventListener("change", () => { 1127 state.month = normalizeArticleMonth(monthSelect.value); 1128 state.page = 1; 1129 renderArticleControls(state.articles); 1130 const monthLabel = ARTICLE_MONTHS.find((month) => month.value === state.month)?.label || ""; 1131 const count = document.querySelector("#articleCount"); 1132 if (count) count.textContent = state.month ? `Loading ${monthLabel} articles...` : "Loading all months..."; 1133 syncNewsControlUrl(); 1134 loadNews(); 1135 }); 1136 document.querySelectorAll("[data-local-filter]").forEach((button) => { 1137 button.addEventListener("click", () => { 1138 state.readerFilter = button.dataset.localFilter || "all"; 1139 state.page = 1; 1140 document.querySelectorAll("[data-local-filter]").forEach((item) => { 1141 item.classList.toggle("active", item === button); 1142 }); 1143 loadNews(); 1144 }); 1145 button.classList.toggle("active", button.dataset.localFilter === state.readerFilter); 1146 }); 1147 document.querySelectorAll(".article-pagination").forEach((control)
1147=> { 1148 control.addEventListener("click", async (event) => { 1149 const button = event.target.closest("[data-page-action]"); 1150 if (!button) return; 1151 const action = button.dataset.pageAction; 1152 if (action === "first") state.page = 1; 1153 else if (action === "last") state.page = state.totalPages; 1154 else state.page += action === "next" ? 1 : -1; 1155 if (state.pagination) await loadNews(); 1156 else renderArticles(state.articles); 1157 scrollNewsResultsIntoView(); 1158 }); 1159 }); 1160 bindNewsTabLinks(); 1161} 1162 1163bindFilters(); 1164loadNews();
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.