File size: 19,637 Bytes
b118820
 
 
 
 
83302c1
b118820
 
3623ae0
b118820
3623ae0
 
 
 
b118820
3623ae0
b118820
3623ae0
b118820
 
 
83302c1
 
3623ae0
 
0db6acb
 
 
83302c1
 
3623ae0
83302c1
 
 
3623ae0
 
 
 
 
 
 
 
 
 
 
83302c1
 
3623ae0
 
 
 
b956a95
 
3623ae0
 
 
7fb1226
 
 
3623ae0
83302c1
3623ae0
 
 
b118820
3623ae0
b118820
3623ae0
f26d6ea
 
 
 
 
 
 
 
 
 
 
b118820
 
 
 
3623ae0
b118820
 
 
 
83302c1
 
0db6acb
 
 
83302c1
 
 
 
 
0db6acb
9e5a278
 
3623ae0
 
 
83302c1
 
b118820
3623ae0
b118820
 
83302c1
3623ae0
b956a95
3623ae0
0db6acb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3623ae0
 
 
b956a95
 
 
3623ae0
b956a95
b118820
 
 
3623ae0
b118820
 
 
3623ae0
b118820
 
3623ae0
b118820
 
3623ae0
 
 
 
 
 
 
 
 
 
 
 
 
 
f91da9c
3623ae0
f91da9c
 
b956a95
 
 
3623ae0
 
 
b118820
83302c1
b956a95
83302c1
b956a95
 
 
83302c1
0db6acb
83302c1
0db6acb
 
 
3623ae0
b118820
 
3623ae0
 
 
 
 
 
 
b118820
 
83302c1
 
 
 
b118820
 
83302c1
 
 
9e5a278
3623ae0
 
 
83302c1
3623ae0
 
0db6acb
 
 
3623ae0
9e5a278
3623ae0
 
 
83302c1
3623ae0
 
 
7fb1226
 
 
 
 
 
 
 
 
 
af675ec
3623ae0
 
 
 
 
 
 
 
9e5a278
3623ae0
 
83302c1
3623ae0
7fb1226
3623ae0
 
 
 
 
83302c1
b118820
 
f26d6ea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
<!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>