nba-buzz / team.html
cdechoch's picture
Update team.html
f26d6ea verified
<!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 = [];
// Team name aliases for matching
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);
// Match URLs with https:// or http://
escaped = escaped.replace(/(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="$1" target="_blank">$1</a>');
// Match domain.com/path URLs (common patterns without protocol)
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>');
// Match @handles (Bluesky handles)
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>');
// Match player names
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>`);
}
// Match full team names (skip current team)
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>`);
}
// Match team aliases (short names like "Lakers", "Celtics") - skip if matches current team
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>';
// Populate period stats
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>