| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Team - NBA Buzz</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; color: #333; } |
| .container { max-width: 900px; margin: 0 auto; } |
| .back-link { display: inline-flex; align-items: center; gap: 8px; color: #f97316; text-decoration: none; font-weight: 600; margin-bottom: 20px; } |
| .back-link:hover { text-decoration: underline; } |
| .search-container { position: relative; margin-bottom: 20px; } |
| .search-input { width: 100%; padding: 12px 16px; font-size: 15px; border: 2px solid #e0e0e0; border-radius: 8px; outline: none; } |
| .search-input:focus { border-color: #f97316; } |
| .search-results { position: absolute; top: 100%; left: 0; right: 0; background: white; border: 2px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px; max-height: 250px; overflow-y: auto; z-index: 100; display: none; } |
| .search-results.active { display: block; } |
| .search-result { padding: 10px 16px; cursor: pointer; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; text-decoration: none; color: inherit; } |
| .search-result:hover { background: #fff8f3; } |
| .search-result .name { font-weight: 600; } |
| .search-result .type { font-size: 12px; color: #888; } |
| .team-header { background: white; border-radius: 12px; padding: 25px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); text-align: center; } |
| .team-name { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 15px; } |
| .mention-count-header { font-size: 48px; font-weight: bold; color: #f97316; } |
| .mention-count-label { font-size: 14px; color: #888; text-transform: uppercase; margin-bottom: 20px; } |
| .period-stats-section { margin-top: 20px; } |
| .period-stats-header { font-size: 14px; color: #666; font-weight: 600; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; } |
| .period-stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; } |
| .period-stat { background: #f8f9fa; border-radius: 8px; padding: 12px; text-align: center; text-decoration: none; display: block; transition: all 0.2s; border: 2px solid #e0e0e0; cursor: pointer; } |
| .period-stat:hover { background: #fff8f3; border-color: #f97316; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(249,115,22,0.2); } |
| .period-label { font-size: 12px; color: #888; font-weight: 600; margin-bottom: 4px; } |
| .period-mentions { font-size: 24px; font-weight: bold; color: #f97316; } |
| .period-mentions-label { font-size: 10px; color: #888; text-transform: uppercase; margin-top: 2px; } |
| .period-rank { font-size: 12px; color: #555; margin-top: 4px; font-weight: 500; } |
| .section-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #f97316; } |
| .mentions-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 30px; } |
| .mention-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } |
| .mention-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } |
| .author-avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; background: #f0f0f0; } |
| .author-info { flex: 1; } |
| .author-name { font-weight: 600; font-size: 15px; color: #333; text-decoration: none; display: block; } |
| .author-name:hover { color: #f97316; } |
| .author-handle { font-size: 13px; color: #888; text-decoration: none; } |
| .author-handle:hover { color: #f97316; } |
| .mention-time { font-size: 12px; color: #aaa; } |
| .player-tag { display: inline-block; background: #f97316; color: white; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 10px; text-decoration: none; } |
| .player-tag:hover { background: #ea580c; } |
| .mention-text { font-size: 15px; line-height: 1.6; color: #333; margin-bottom: 12px; white-space: pre-wrap; word-wrap: break-word; } |
| .mention-text a { color: #f97316; text-decoration: none; } |
| .mention-text a:hover { text-decoration: underline; } |
| .mention-text .player-link { color: #2563eb; font-weight: 500; } |
| .mention-text .team-link { color: #059669; font-weight: 500; } |
| .mention-text .handle-link { color: #7c3aed; } |
| .mention-footer { margin-top: 12px; } |
| .view-original { color: #f97316; text-decoration: none; font-size: 14px; font-weight: 500; } |
| .view-original:hover { text-decoration: underline; } |
| .quote-post { background: #f8f9fa; border-left: 3px solid #ddd; padding: 12px 15px; margin: 12px 0; border-radius: 0 8px 8px 0; } |
| .quote-author { font-weight: 600; font-size: 13px; color: #666; margin-bottom: 6px; } |
| .quote-text { font-size: 14px; line-height: 1.5; color: #555; } |
| .related-section { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } |
| .related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; } |
| .related-link { display: block; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; text-decoration: none; color: #333; font-weight: 500; } |
| .related-link:hover { background: #fff8f3; color: #f97316; } |
| .bottom-nav { text-align: center; padding: 20px; border-top: 1px solid #eee; margin-top: 20px; } |
| .loading { text-align: center; padding: 40px; color: #888; } |
| .spinner { width: 40px; height: 40px; border: 4px solid #f0f0f0; border-top-color: #f97316; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 15px; } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .empty-state { text-align: center; padding: 40px; background: white; border-radius: 12px; } |
| |
| @media (max-width: 600px) { |
| .period-stats { gap: 5px; } |
| .period-stat { padding: 8px 4px; } |
| .period-label { font-size: 10px; } |
| .period-mentions { font-size: 16px; } |
| .period-mentions-label { font-size: 8px; } |
| .period-rank { font-size: 9px; } |
| .team-name { font-size: 24px; } |
| .team-header { padding: 15px; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <a href="/" class="back-link">← Back to Rankings</a> |
| <div class="search-container"> |
| <input type="text" class="search-input" id="searchInput" placeholder="Search players or teams..."> |
| <div class="search-results" id="searchResults"></div> |
| </div> |
| <div class="team-header"> |
| <div class="team-name" id="teamName">Loading...</div> |
| <div class="period-stats-section"> |
| <div class="period-stats-header">Mentions by Time Period</div> |
| <div class="period-stats" id="periodStats"> |
| <a href="/?hours=6&tab=teams" class="period-stat"><div class="period-label">6h</div><div class="period-mentions">-</div><div class="period-rank"></div></a> |
| <a href="/?hours=12&tab=teams" class="period-stat"><div class="period-label">12h</div><div class="period-mentions">-</div><div class="period-rank"></div></a> |
| <a href="/?hours=24&tab=teams" class="period-stat"><div class="period-label">24h</div><div class="period-mentions">-</div><div class="period-rank"></div></a> |
| <a href="/?hours=48&tab=teams" class="period-stat"><div class="period-label">48h</div><div class="period-mentions">-</div><div class="period-rank"></div></a> |
| <a href="/?hours=168&tab=teams" class="period-stat"><div class="period-label">7d</div><div class="period-mentions">-</div><div class="period-rank"></div></a> |
| </div> |
| </div> |
| </div> |
| <div class="section-title">Recent Mentions</div> |
| <div class="mentions-list" id="mentionsList"><div class="loading"><div class="spinner"></div><p>Loading...</p></div></div> |
| <div class="related-section"> |
| <div class="section-title" style="border:none;margin:0 0 15px 0;padding:0;">Roster</div> |
| <div class="related-grid" id="rosterGrid"><div class="loading">Loading...</div></div> |
| </div> |
| <div class="bottom-nav"><a href="/" class="back-link">← Back to Rankings</a></div> |
| </div> |
| <script> |
| const teamName = decodeURIComponent(window.location.pathname.split('/team/')[1] || ''); |
| let allPlayers = []; |
| let allTeams = []; |
| |
| |
| const TEAM_ALIASES = { |
| 'lakers': 'Los Angeles Lakers', 'clippers': 'Los Angeles Clippers', |
| 'warriors': 'Golden State Warriors', 'kings': 'Sacramento Kings', |
| 'suns': 'Phoenix Suns', 'mavs': 'Dallas Mavericks', 'mavericks': 'Dallas Mavericks', |
| 'rockets': 'Houston Rockets', 'spurs': 'San Antonio Spurs', 'san antonio': 'San Antonio Spurs', |
| 'grizzlies': 'Memphis Grizzlies', 'pelicans': 'New Orleans Pelicans', 'new orleans': 'New Orleans Pelicans', |
| 'thunder': 'Oklahoma City Thunder', 'okc': 'Oklahoma City Thunder', 'oklahoma city': 'Oklahoma City Thunder', |
| 'nuggets': 'Denver Nuggets', 'timberwolves': 'Minnesota Timberwolves', 'wolves': 'Minnesota Timberwolves', 'minnesota': 'Minnesota Timberwolves', |
| 'blazers': 'Portland Trail Blazers', 'trail blazers': 'Portland Trail Blazers', 'portland': 'Portland Trail Blazers', |
| 'jazz': 'Utah Jazz', 'celtics': 'Boston Celtics', 'boston': 'Boston Celtics', |
| 'nets': 'Brooklyn Nets', 'brooklyn': 'Brooklyn Nets', 'knicks': 'New York Knicks', 'new york': 'New York Knicks', |
| 'sixers': 'Philadelphia 76ers', '76ers': 'Philadelphia 76ers', 'philly': 'Philadelphia 76ers', 'philadelphia': 'Philadelphia 76ers', |
| 'raptors': 'Toronto Raptors', 'toronto': 'Toronto Raptors', |
| 'bulls': 'Chicago Bulls', 'chicago': 'Chicago Bulls', 'cavs': 'Cleveland Cavaliers', 'cavaliers': 'Cleveland Cavaliers', 'cleveland': 'Cleveland Cavaliers', |
| 'pistons': 'Detroit Pistons', 'detroit': 'Detroit Pistons', 'pacers': 'Indiana Pacers', 'indiana': 'Indiana Pacers', |
| 'bucks': 'Milwaukee Bucks', 'milwaukee': 'Milwaukee Bucks', |
| 'hawks': 'Atlanta Hawks', 'atlanta': 'Atlanta Hawks', 'hornets': 'Charlotte Hornets', 'charlotte': 'Charlotte Hornets', |
| 'heat': 'Miami Heat', 'miami': 'Miami Heat', 'magic': 'Orlando Magic', 'orlando': 'Orlando Magic', |
| 'wizards': 'Washington Wizards', 'washington': 'Washington Wizards' |
| }; |
| |
| async function loadPlayerList() { |
| try { const res = await fetch('/api/players?hours=72&limit=200'); allPlayers = (await res.json()).map(p => p.player); } catch (e) {} |
| } |
| async function loadTeamList() { |
| try { const res = await fetch('/api/teams?hours=168&limit=30'); allTeams = (await res.json()).map(t => t.team); } catch (e) {} |
| } |
| loadPlayerList(); |
| loadTeamList(); |
| |
| const searchInput = document.getElementById('searchInput'); |
| const searchResults = document.getElementById('searchResults'); |
| let searchTimeout; |
| searchInput.addEventListener('input', (e) => { |
| clearTimeout(searchTimeout); |
| const q = e.target.value.trim(); |
| if (q.length < 2) { searchResults.classList.remove('active'); return; } |
| searchTimeout = setTimeout(() => doSearch(q), 200); |
| }); |
| document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) searchResults.classList.remove('active'); }); |
| |
| async function doSearch(q) { |
| const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`); |
| const data = await res.json(); |
| let html = ''; |
| data.players.forEach(p => { html += `<a href="/player/${encodeURIComponent(p)}" class="search-result"><span class="name">${p}</span><span class="type">Player</span></a>`; }); |
| data.teams.forEach(t => { html += `<a href="/team/${encodeURIComponent(t)}" class="search-result"><span class="name">${t}</span><span class="type">Team</span></a>`; }); |
| searchResults.innerHTML = html || '<div class="search-result">No results</div>'; |
| searchResults.classList.add('active'); |
| } |
| |
| function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } |
| |
| function processText(text) { |
| if (!text) return ''; |
| let escaped = escapeHtml(text); |
| |
| escaped = escaped.replace(/(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="$1" target="_blank">$1</a>'); |
| |
| escaped = escaped.replace(/(?<![\/\w@])((?:[a-zA-Z0-9-]+\.)+(?:com|net|org|io|co|tv|be|ly|me|us|uk|ca|au|de|fr|es|it|nl|app|dev|gg|xyz|info|biz)\/[^\s<]*[^<.,:;"')\]\s])/gi, '<a href="https://$1" target="_blank">$1</a>'); |
| |
| escaped = escaped.replace(/@([a-zA-Z0-9][a-zA-Z0-9._-]*(?:\.[a-zA-Z][a-zA-Z0-9._-]*)+)/g, '<a href="https://bsky.app/profile/$1" target="_blank" class="handle-link">@$1</a>'); |
| |
| for (const player of allPlayers) { |
| const regex = new RegExp(`\\b(${player.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi'); |
| escaped = escaped.replace(regex, `<a href="/player/${encodeURIComponent(player)}" class="player-link">$1</a>`); |
| } |
| |
| for (const team of allTeams) { |
| if (team === teamName) continue; |
| const regex = new RegExp(`\\b(${team.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi'); |
| escaped = escaped.replace(regex, `<a href="/team/${encodeURIComponent(team)}" class="team-link">$1</a>`); |
| } |
| |
| for (const [alias, fullName] of Object.entries(TEAM_ALIASES)) { |
| if (fullName === teamName) continue; |
| const regex = new RegExp(`(?<!<[^>]*)\\b(${alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b(?![^<]*>)`, 'gi'); |
| escaped = escaped.replace(regex, `<a href="/team/${encodeURIComponent(fullName)}" class="team-link">$1</a>`); |
| } |
| return escaped; |
| } |
| |
| function timeAgo(d) { |
| if (!d) return ''; |
| const s = Math.floor((new Date() - new Date(d)) / 1000); |
| if (s < 60) return 'just now'; |
| if (s < 3600) return Math.floor(s/60) + 'm ago'; |
| if (s < 86400) return Math.floor(s/3600) + 'h ago'; |
| return Math.floor(s/86400) + 'd ago'; |
| } |
| |
| async function loadTeam() { |
| if (!teamName) { document.getElementById('teamName').textContent = 'Not found'; return; } |
| document.getElementById('teamName').textContent = teamName; |
| document.title = teamName + ' - NBA Buzz'; |
| |
| try { |
| const info = await (await fetch(`/api/team/${encodeURIComponent(teamName)}`)).json(); |
| if (info.players?.length) document.getElementById('rosterGrid').innerHTML = info.players.map(p => `<a href="/player/${encodeURIComponent(p)}" class="related-link">${p}</a>`).join(''); |
| else document.getElementById('rosterGrid').innerHTML = '<p style="color:#888">No players</p>'; |
| |
| |
| if (info.period_stats) { |
| document.getElementById('periodStats').innerHTML = info.period_stats.map(p => ` |
| <a href="/?hours=${p.hours}&tab=teams" class="period-stat"> |
| <div class="period-label">${p.label}</div> |
| <div class="period-mentions">${p.mentions}</div> |
| <div class="period-mentions-label">mentions</div> |
| <div class="period-rank">${p.rank ? 'Rank #' + p.rank : '-'}</div> |
| </a> |
| `).join(''); |
| } |
| } catch (e) { console.error(e); } |
| |
| try { |
| const mentions = await (await fetch(`/api/team-mentions/${encodeURIComponent(teamName)}?limit=50`)).json(); |
| if (!mentions.length) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>No mentions found</h3></div>'; return; } |
| document.getElementById('mentionsList').innerHTML = mentions.map(m => { |
| const avatarUrl = m.author_avatar || 'https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg'; |
| let quoteHtml = ''; |
| if (m.quote_post && m.quote_post.text) { |
| const qAuthor = m.quote_post.author_name || m.quote_post.author_handle || 'Unknown'; |
| quoteHtml = ` |
| <div class="quote-post"> |
| <div class="quote-author">↩ ${escapeHtml(qAuthor)}</div> |
| <div class="quote-text">${processText(m.quote_post.text)}</div> |
| </div> |
| `; |
| } |
| return ` |
| <div class="mention-card"> |
| <div class="mention-header"> |
| <a href="https://bsky.app/profile/${m.author_handle}" target="_blank"> |
| <img src="${avatarUrl}" class="author-avatar" onerror="this.src='https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg'"> |
| </a> |
| <div class="author-info"> |
| <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-name">${escapeHtml(m.author)}</a> |
| <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-handle">@${escapeHtml(m.author_handle?.split('.')[0] || '')}</a> |
| </div> |
| <span class="mention-time">${timeAgo(m.created_at)}</span> |
| </div> |
| ${m.player ? `<a href="/player/${encodeURIComponent(m.player)}" class="player-tag">${escapeHtml(m.player)}</a>` : ''} |
| <div class="mention-text">${processText(m.text)}</div> |
| ${quoteHtml} |
| <div class="mention-footer"><a href="${m.url}" target="_blank" class="view-original">View on Bluesky →</a></div> |
| </div> |
| `}).join(''); |
| } catch (e) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>Error loading</h3></div>'; } |
| } |
| loadTeam(); |
| </script> |
| </body> |
| </html> |