File size: 19,552 Bytes
b118820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83c7b7f
29b39e0
 
 
 
 
83c7b7f
29b39e0
 
 
b118820
 
 
 
1d4d491
b118820
1d4d491
 
 
 
b118820
 
 
 
 
e055c20
 
b118820
 
 
f9ea4b7
 
 
b118820
 
 
 
 
 
 
 
 
2945930
 
 
 
 
 
 
 
 
 
 
b118820
 
 
 
 
 
 
 
 
 
 
 
29b39e0
 
 
 
 
 
 
 
 
83c7b7f
b118820
 
 
 
 
 
 
 
 
 
 
 
e055c20
b118820
29b39e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b118820
 
 
e055c20
 
 
b118820
e055c20
b118820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6936546
83c7b7f
6936546
 
e055c20
 
 
b118820
 
 
 
 
29b39e0
e055c20
 
 
 
29b39e0
 
 
 
 
b118820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83c7b7f
 
 
 
29b39e0
83c7b7f
 
29b39e0
 
 
83c7b7f
 
 
b118820
 
 
 
1d4d491
 
f9ea4b7
 
 
 
 
 
 
 
 
 
1d4d491
b118820
 
1d4d491
 
 
 
 
 
 
b118820
 
 
f9ea4b7
b118820
 
1d4d491
b118820
 
 
 
 
2945930
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
269
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Player - 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; }
        .player-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; }
        .player-name { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 8px; }
        .player-team { font-size: 18px; color: #f97316; margin-bottom: 15px; }
        .player-team a { color: #f97316; text-decoration: none; }
        .player-team a:hover { text-decoration: underline; }
        .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; }
        .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(180px, 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; }
            .player-name { font-size: 24px; }
            .player-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="player-header">
            <div class="player-name" id="playerName">Loading...</div>
            <div class="player-team" id="playerTeam"></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" class="period-stat"><div class="period-label">6h</div><div class="period-mentions">-</div><div class="period-rank"></div></a>
                    <a href="/?hours=12" class="period-stat"><div class="period-label">12h</div><div class="period-mentions">-</div><div class="period-rank"></div></a>
                    <a href="/?hours=24" class="period-stat"><div class="period-label">24h</div><div class="period-mentions">-</div><div class="period-rank"></div></a>
                    <a href="/?hours=48" class="period-stat"><div class="period-label">48h</div><div class="period-mentions">-</div><div class="period-rank"></div></a>
                    <a href="/?hours=168" 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;">Teammates</div>
            <div class="related-grid" id="relatedGrid"><div class="loading">Loading...</div></div>
        </div>
        <div class="bottom-nav"><a href="/" class="back-link">← Back to Rankings</a></div>
    </div>
    <script>
        const playerName = decodeURIComponent(window.location.pathname.split('/player/')[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) {
                if (player === playerName) continue;
                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 first
            for (const team of allTeams) {
                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")
            for (const [alias, fullName] of Object.entries(TEAM_ALIASES)) {
                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 loadPlayer() {
            if (!playerName) { document.getElementById('playerName').textContent = 'Not found'; return; }
            document.getElementById('playerName').textContent = playerName;
            document.title = playerName + ' - NBA Buzz';
            
            try {
                const info = await (await fetch(`/api/player/${encodeURIComponent(playerName)}`)).json();
                if (info.team) document.getElementById('playerTeam').innerHTML = `<a href="/team/${encodeURIComponent(info.team)}">${info.team}</a>`;
                if (info.teammates?.length) document.getElementById('relatedGrid').innerHTML = info.teammates.map(t => `<a href="/player/${encodeURIComponent(t)}" class="related-link">${t}</a>`).join('');
                else document.getElementById('relatedGrid').innerHTML = '<p style="color:#888">No teammates</p>';
                
                // Populate period stats
                if (info.period_stats) {
                    document.getElementById('periodStats').innerHTML = info.period_stats.map(p => `
                        <a href="/?hours=${p.hours}" 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/mentions/${encodeURIComponent(playerName)}?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>
                        <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>'; }
        }
        loadPlayer();
    </script>
</body>
</html>