cdechoch commited on
Commit
9e5a278
Β·
verified Β·
1 Parent(s): cac02e5

Update team.html

Browse files
Files changed (1) hide show
  1. team.html +406 -123
team.html CHANGED
@@ -3,171 +3,454 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Team - NBA Buzz</title>
7
  <style>
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; color: #333; }
 
 
 
 
 
10
  .container { max-width: 900px; margin: 0 auto; }
11
- .back-link { display: inline-flex; align-items: center; gap: 8px; color: #f97316; text-decoration: none; font-weight: 600; margin-bottom: 20px; }
12
- .back-link:hover { text-decoration: underline; }
13
- .search-container { position: relative; margin-bottom: 20px; }
14
- .search-input { width: 100%; padding: 12px 16px; font-size: 15px; border: 2px solid #e0e0e0; border-radius: 8px; outline: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  .search-input:focus { border-color: #f97316; }
16
- .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; }
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  .search-results.active { display: block; }
18
- .search-result { padding: 10px 16px; cursor: pointer; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; text-decoration: none; color: inherit; }
 
 
 
 
 
 
 
 
19
  .search-result:hover { background: #fff8f3; }
20
  .search-result .name { font-weight: 600; }
21
  .search-result .type { font-size: 12px; color: #888; }
22
- .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; }
23
- .team-name { font-size: 32px; font-weight: bold; color: #333; margin-bottom: 15px; }
24
- .mention-count-header { font-size: 48px; font-weight: bold; color: #f97316; }
25
- .mention-count-label { font-size: 14px; color: #888; text-transform: uppercase; }
26
- .section-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #f97316; }
27
- .mentions-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 30px; }
28
- .mention-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
29
- .mention-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
30
- .author-avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; background: #f0f0f0; }
31
- .author-info { flex: 1; }
32
- .author-name { font-weight: 600; font-size: 15px; color: #333; text-decoration: none; display: block; }
33
- .author-name:hover { color: #f97316; }
34
- .author-handle { font-size: 13px; color: #888; text-decoration: none; }
35
- .author-handle:hover { color: #f97316; }
36
- .mention-time { font-size: 12px; color: #aaa; }
37
- .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; }
38
- .player-tag:hover { background: #ea580c; }
39
- .mention-text { font-size: 15px; line-height: 1.6; color: #333; margin-bottom: 12px; white-space: pre-wrap; word-wrap: break-word; }
40
- .mention-text a { color: #f97316; text-decoration: none; }
41
- .mention-text a:hover { text-decoration: underline; }
42
- .mention-text .player-link { color: #2563eb; font-weight: 500; }
43
- .mention-footer { margin-top: 12px; }
44
- .view-original { color: #f97316; text-decoration: none; font-size: 14px; font-weight: 500; }
45
- .view-original:hover { text-decoration: underline; }
46
- .related-section { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
47
- .related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
48
- .related-link { display: block; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; text-decoration: none; color: #333; font-weight: 500; }
49
- .related-link:hover { background: #fff8f3; color: #f97316; }
50
- .bottom-nav { text-align: center; padding: 20px; border-top: 1px solid #eee; margin-top: 20px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  .loading { text-align: center; padding: 40px; color: #888; }
52
- .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; }
 
 
 
 
 
 
 
 
53
  @keyframes spin { to { transform: rotate(360deg); } }
54
- .empty-state { text-align: center; padding: 40px; background: white; border-radius: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  </style>
56
  </head>
57
  <body>
58
  <div class="container">
59
- <a href="/" class="back-link">← Back to Rankings</a>
 
 
 
 
60
  <div class="search-container">
61
  <input type="text" class="search-input" id="searchInput" placeholder="Search players or teams...">
62
  <div class="search-results" id="searchResults"></div>
63
  </div>
64
- <div class="team-header">
65
- <div class="team-name" id="teamName">Loading...</div>
66
- <div class="mention-count-header" id="totalMentions">-</div>
67
- <div class="mention-count-label">mentions</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  </div>
69
- <div class="section-title">Recent Mentions</div>
70
- <div class="mentions-list" id="mentionsList"><div class="loading"><div class="spinner"></div><p>Loading...</p></div></div>
71
- <div class="related-section">
72
- <div class="section-title" style="border:none;margin:0 0 15px 0;padding:0;">Roster</div>
73
- <div class="related-grid" id="rosterGrid"><div class="loading">Loading...</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
- <div class="bottom-nav"><a href="/" class="back-link">← Back to Rankings</a></div>
76
  </div>
 
77
  <script>
78
- const teamName = decodeURIComponent(window.location.pathname.split('/team/')[1] || '');
79
- let allPlayers = [];
80
-
81
- async function loadPlayerList() {
82
- try { const res = await fetch('/api/players?hours=72&limit=200'); allPlayers = (await res.json()).map(p => p.player); } catch (e) {}
83
- }
84
- loadPlayerList();
85
 
 
86
  const searchInput = document.getElementById('searchInput');
87
  const searchResults = document.getElementById('searchResults');
88
- let searchTimeout;
89
  searchInput.addEventListener('input', (e) => {
90
  clearTimeout(searchTimeout);
91
  const q = e.target.value.trim();
92
- if (q.length < 2) { searchResults.classList.remove('active'); return; }
 
 
 
93
  searchTimeout = setTimeout(() => doSearch(q), 200);
94
  });
95
- document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) searchResults.classList.remove('active'); });
 
 
 
 
 
 
 
 
 
96
 
97
  async function doSearch(q) {
98
- const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
99
- const data = await res.json();
100
- let html = '';
101
- data.players.forEach(p => { html += `<a href="/player/${encodeURIComponent(p)}" class="search-result"><span class="name">${p}</span><span class="type">Player</span></a>`; });
102
- data.teams.forEach(t => { html += `<a href="/team/${encodeURIComponent(t)}" class="search-result"><span class="name">${t}</span><span class="type">Team</span></a>`; });
103
- searchResults.innerHTML = html || '<div class="search-result">No results</div>';
104
- searchResults.classList.add('active');
105
- }
106
-
107
- function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
108
-
109
- function processText(text) {
110
- if (!text) return '';
111
- let escaped = escapeHtml(text);
112
- // Match URLs - https://, http://, and common short domains
113
- escaped = escaped.replace(/(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="$1" target="_blank">$1</a>');
114
- // Match youtu.be, bit.ly, etc without protocol
115
- escaped = escaped.replace(/(?<![\/\w])((?:youtu\.be|bit\.ly|t\.co|tinyurl\.com|goo\.gl)\/[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="https://$1" target="_blank">$1</a>');
116
- // Match www. URLs
117
- escaped = escaped.replace(/(?<![\/\w])(www\.[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="https://$1" target="_blank">$1</a>');
118
- for (const player of allPlayers) {
119
- const regex = new RegExp(`\\b(${player.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi');
120
- escaped = escaped.replace(regex, `<a href="/player/${encodeURIComponent(player)}" class="player-link">$1</a>`);
121
  }
122
- return escaped;
123
  }
124
 
125
- function timeAgo(d) {
126
- if (!d) return '';
127
- const s = Math.floor((new Date() - new Date(d)) / 1000);
128
- if (s < 60) return 'just now';
129
- if (s < 3600) return Math.floor(s/60) + 'm ago';
130
- if (s < 86400) return Math.floor(s/3600) + 'h ago';
131
- return Math.floor(s/86400) + 'd ago';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  }
133
 
134
- async function loadTeam() {
135
- if (!teamName) { document.getElementById('teamName').textContent = 'Not found'; return; }
136
- document.getElementById('teamName').textContent = teamName;
137
- document.title = teamName + ' - NBA Buzz';
138
 
139
- try {
140
- const info = await (await fetch(`/api/team/${encodeURIComponent(teamName)}`)).json();
141
- if (info.players?.length) document.getElementById('rosterGrid').innerHTML = info.players.map(p => `<a href="/player/${encodeURIComponent(p)}" class="related-link">${p}</a>`).join('');
142
- else document.getElementById('rosterGrid').innerHTML = '<p style="color:#888">No players</p>';
143
- } catch (e) {}
144
 
145
  try {
146
- const mentions = await (await fetch(`/api/team-mentions/${encodeURIComponent(teamName)}?limit=50`)).json();
147
- document.getElementById('totalMentions').textContent = mentions.length;
148
- if (!mentions.length) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>No mentions found</h3></div>'; return; }
149
- document.getElementById('mentionsList').innerHTML = mentions.map(m => {
150
- const avatarUrl = m.author_avatar || 'https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg';
 
 
 
 
 
 
 
 
151
  return `
152
- <div class="mention-card">
153
- <div class="mention-header">
154
- <a href="https://bsky.app/profile/${m.author_handle}" target="_blank">
155
- <img src="${avatarUrl}" class="author-avatar" onerror="this.src='https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg'">
156
- </a>
157
- <div class="author-info">
158
- <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-name">${escapeHtml(m.author)}</a>
159
- <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-handle">@${escapeHtml(m.author_handle?.split('.')[0] || '')}</a>
160
  </div>
161
- <span class="mention-time">${timeAgo(m.created_at)}</span>
162
- </div>
163
- ${m.player ? `<a href="/player/${encodeURIComponent(m.player)}" class="player-tag">${escapeHtml(m.player)}</a>` : ''}
164
- <div class="mention-text">${processText(m.text)}</div>
165
- <div class="mention-footer"><a href="${m.url}" target="_blank" class="view-original">View on Bluesky β†’</a></div>
166
- </div>
167
- `}).join('');
168
- } catch (e) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>Error loading</h3></div>'; }
169
- }
170
- loadTeam();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </script>
172
  </body>
173
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NBA Buzz - Player Mentions Tracker</title>
7
  <style>
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #f5f5f5;
12
+ padding: 20px;
13
+ color: #333;
14
+ }
15
  .container { max-width: 900px; margin: 0 auto; }
16
+ .header {
17
+ text-align: center;
18
+ margin-bottom: 25px;
19
+ padding-bottom: 20px;
20
+ border-bottom: 3px solid #f97316;
21
+ }
22
+ .header h1 { font-size: 32px; color: #2c3e50; margin-bottom: 8px; }
23
+ .header h1 span { color: #f97316; }
24
+ .header p { color: #7f8c8d; font-size: 15px; }
25
+
26
+ .search-container {
27
+ position: relative;
28
+ margin-bottom: 20px;
29
+ }
30
+ .search-input {
31
+ width: 100%;
32
+ padding: 15px 20px;
33
+ font-size: 16px;
34
+ border: 2px solid #e0e0e0;
35
+ border-radius: 10px;
36
+ outline: none;
37
+ }
38
  .search-input:focus { border-color: #f97316; }
39
+ .search-results {
40
+ position: absolute;
41
+ top: 100%;
42
+ left: 0;
43
+ right: 0;
44
+ background: white;
45
+ border: 2px solid #e0e0e0;
46
+ border-top: none;
47
+ border-radius: 0 0 10px 10px;
48
+ max-height: 300px;
49
+ overflow-y: auto;
50
+ z-index: 100;
51
+ display: none;
52
+ }
53
  .search-results.active { display: block; }
54
+ .search-result {
55
+ padding: 12px 20px;
56
+ cursor: pointer;
57
+ border-bottom: 1px solid #f0f0f0;
58
+ display: flex;
59
+ justify-content: space-between;
60
+ text-decoration: none;
61
+ color: inherit;
62
+ }
63
  .search-result:hover { background: #fff8f3; }
64
  .search-result .name { font-weight: 600; }
65
  .search-result .type { font-size: 12px; color: #888; }
66
+
67
+ .stats-banner {
68
+ display: grid;
69
+ grid-template-columns: repeat(4, 1fr);
70
+ gap: 15px;
71
+ margin-bottom: 20px;
72
+ }
73
+ .stat-box {
74
+ text-align: center;
75
+ padding: 15px;
76
+ background: white;
77
+ border-radius: 8px;
78
+ box-shadow: 0 2px 4px rgba(0,0,0,0.08);
79
+ }
80
+ .stat-box .number { font-size: 24px; font-weight: bold; color: #f97316; }
81
+ .stat-box .label { font-size: 11px; color: #7f8c8d; margin-top: 5px; text-transform: uppercase; }
82
+
83
+ .status-bar {
84
+ text-align: center;
85
+ padding: 10px;
86
+ font-size: 13px;
87
+ color: #888;
88
+ background: #f8f9fa;
89
+ border-radius: 6px;
90
+ margin-bottom: 20px;
91
+ }
92
+ .status-bar.fetching { background: #fef3c7; color: #92400e; }
93
+
94
+ .controls {
95
+ background: white;
96
+ padding: 15px 20px;
97
+ border-radius: 10px;
98
+ margin-bottom: 20px;
99
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
100
+ display: flex;
101
+ gap: 15px;
102
+ align-items: center;
103
+ flex-wrap: wrap;
104
+ }
105
+ .control-group { display: flex; align-items: center; gap: 8px; }
106
+ .control-group label { font-size: 13px; font-weight: 600; color: #555; }
107
+ .time-filters { display: flex; gap: 5px; }
108
+ .time-btn {
109
+ padding: 6px 12px;
110
+ border: 2px solid #e0e0e0;
111
+ background: white;
112
+ border-radius: 6px;
113
+ cursor: pointer;
114
+ font-size: 13px;
115
+ font-weight: 500;
116
+ }
117
+ .time-btn:hover { border-color: #f97316; color: #f97316; }
118
+ .time-btn.active { background: #f97316; color: white; border-color: #f97316; }
119
+ .tab-buttons { display: flex; gap: 5px; margin-left: auto; }
120
+ .tab-btn {
121
+ padding: 8px 20px;
122
+ border: 2px solid #e0e0e0;
123
+ background: white;
124
+ border-radius: 6px;
125
+ cursor: pointer;
126
+ font-size: 14px;
127
+ font-weight: 500;
128
+ }
129
+ .tab-btn:hover { border-color: #f97316; color: #f97316; }
130
+ .tab-btn.active { background: #f97316; color: white; border-color: #f97316; }
131
+ .refresh-btn {
132
+ padding: 8px 16px;
133
+ background: #f97316;
134
+ color: white;
135
+ border: none;
136
+ border-radius: 6px;
137
+ cursor: pointer;
138
+ font-size: 14px;
139
+ font-weight: 600;
140
+ }
141
+ .refresh-btn:hover { background: #ea580c; }
142
+ .refresh-btn:disabled { background: #ccc; }
143
+
144
+ .rankings-panel {
145
+ background: white;
146
+ border-radius: 10px;
147
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
148
+ overflow: hidden;
149
+ }
150
+ .panel-header {
151
+ background: #f8f9fa;
152
+ padding: 15px 20px;
153
+ font-weight: 600;
154
+ font-size: 16px;
155
+ color: #333;
156
+ border-bottom: 1px solid #eee;
157
+ }
158
+ .ranking-item {
159
+ display: flex;
160
+ align-items: center;
161
+ padding: 15px 20px;
162
+ border-bottom: 1px solid #f0f0f0;
163
+ text-decoration: none;
164
+ color: inherit;
165
+ transition: background 0.2s;
166
+ }
167
+ .ranking-item:hover { background: #fff8f3; }
168
+ .rank-num {
169
+ width: 32px;
170
+ height: 32px;
171
+ background: #f0f0f0;
172
+ border-radius: 50%;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ font-size: 13px;
177
+ font-weight: 600;
178
+ color: #666;
179
+ margin-right: 15px;
180
+ }
181
+ .player-info { flex: 1; }
182
+ .player-name { font-weight: 600; font-size: 15px; color: #333; }
183
+ .player-team { font-size: 13px; color: #f97316; margin-top: 2px; }
184
+ .mention-count { text-align: right; margin-right: 20px; }
185
+ .mention-num { font-size: 20px; font-weight: bold; color: #f97316; }
186
+ .mention-label { font-size: 10px; color: #888; text-transform: uppercase; }
187
+ .sentiment-dots { display: flex; gap: 12px; }
188
+ .sentiment-dot { font-size: 12px; font-weight: 500; }
189
+ .sentiment-dot.positive { color: #22c55e; }
190
+ .sentiment-dot.neutral { color: #888; }
191
+ .sentiment-dot.negative { color: #ef4444; }
192
+
193
  .loading { text-align: center; padding: 40px; color: #888; }
194
+ .spinner {
195
+ width: 40px;
196
+ height: 40px;
197
+ border: 4px solid #f0f0f0;
198
+ border-top-color: #f97316;
199
+ border-radius: 50%;
200
+ animation: spin 1s linear infinite;
201
+ margin: 0 auto 15px;
202
+ }
203
  @keyframes spin { to { transform: rotate(360deg); } }
204
+ .empty-state { text-align: center; padding: 40px; color: #888; }
205
+ .empty-state h3 { margin-bottom: 10px; color: #666; }
206
+
207
+ .legal-note {
208
+ text-align: center;
209
+ font-size: 11px;
210
+ color: #aaa;
211
+ margin-top: 20px;
212
+ padding-top: 15px;
213
+ border-top: 1px solid #eee;
214
+ }
215
+
216
+ @media (max-width: 600px) {
217
+ .stats-banner { grid-template-columns: repeat(2, 1fr); }
218
+ .controls { flex-direction: column; align-items: stretch; }
219
+ .tab-buttons { margin-left: 0; justify-content: center; }
220
+ }
221
  </style>
222
  </head>
223
  <body>
224
  <div class="container">
225
+ <div class="header">
226
+ <h1>πŸ€ NBA <span>Buzz</span></h1>
227
+ <p>Real-time player mentions from Bluesky</p>
228
+ </div>
229
+
230
  <div class="search-container">
231
  <input type="text" class="search-input" id="searchInput" placeholder="Search players or teams...">
232
  <div class="search-results" id="searchResults"></div>
233
  </div>
234
+
235
+ <div id="statusBar" class="status-bar">Loading...</div>
236
+
237
+ <div class="stats-banner">
238
+ <div class="stat-box">
239
+ <div class="number" id="statPosts">-</div>
240
+ <div class="label">Posts</div>
241
+ </div>
242
+ <div class="stat-box">
243
+ <div class="number" id="statMentions">-</div>
244
+ <div class="label">Mentions</div>
245
+ </div>
246
+ <div class="stat-box">
247
+ <div class="number" id="statPlayers">-</div>
248
+ <div class="label">Players</div>
249
+ </div>
250
+ <div class="stat-box">
251
+ <div class="number" id="statUpdated">-</div>
252
+ <div class="label">Updated</div>
253
+ </div>
254
  </div>
255
+
256
+ <div class="controls">
257
+ <div class="control-group">
258
+ <label>Time:</label>
259
+ <div class="time-filters">
260
+ <button class="time-btn" data-hours="6">6h</button>
261
+ <button class="time-btn" data-hours="12">12h</button>
262
+ <button class="time-btn active" data-hours="24">24h</button>
263
+ <button class="time-btn" data-hours="48">48h</button>
264
+ <button class="time-btn" data-hours="168">7d</button>
265
+ </div>
266
+ </div>
267
+
268
+ <div class="tab-buttons">
269
+ <button class="tab-btn active" data-tab="players">πŸ‘€ Players</button>
270
+ <button class="tab-btn" data-tab="teams">πŸ† Teams</button>
271
+ </div>
272
+
273
+ <button class="refresh-btn" id="refreshBtn">πŸ”„ Refresh</button>
274
+ </div>
275
+
276
+ <div class="rankings-panel">
277
+ <div class="panel-header" id="rankingsHeader">Trending Players</div>
278
+ <div id="rankingsList">
279
+ <div class="loading">
280
+ <div class="spinner"></div>
281
+ <p>Loading...</p>
282
+ </div>
283
+ </div>
284
+ </div>
285
+
286
+ <div class="legal-note">
287
+ Data sourced from Bluesky's public AT Protocol API. All posts are publicly available.
288
  </div>
 
289
  </div>
290
+
291
  <script>
292
+ let currentTab = 'players';
293
+ let hours = 24;
294
+ let searchTimeout;
 
 
 
 
295
 
296
+ // Search functionality
297
  const searchInput = document.getElementById('searchInput');
298
  const searchResults = document.getElementById('searchResults');
299
+
300
  searchInput.addEventListener('input', (e) => {
301
  clearTimeout(searchTimeout);
302
  const q = e.target.value.trim();
303
+ if (q.length < 2) {
304
+ searchResults.classList.remove('active');
305
+ return;
306
+ }
307
  searchTimeout = setTimeout(() => doSearch(q), 200);
308
  });
309
+
310
+ searchInput.addEventListener('focus', () => {
311
+ if (searchInput.value.length >= 2) searchResults.classList.add('active');
312
+ });
313
+
314
+ document.addEventListener('click', (e) => {
315
+ if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
316
+ searchResults.classList.remove('active');
317
+ }
318
+ });
319
 
320
  async function doSearch(q) {
321
+ try {
322
+ const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
323
+ const data = await res.json();
324
+
325
+ let html = '';
326
+ data.players.forEach(p => {
327
+ html += `<a href="/player/${encodeURIComponent(p)}" class="search-result">
328
+ <span class="name">${p}</span>
329
+ <span class="type">Player</span>
330
+ </a>`;
331
+ });
332
+ data.teams.forEach(t => {
333
+ html += `<a href="/team/${encodeURIComponent(t)}" class="search-result">
334
+ <span class="name">${t}</span>
335
+ <span class="type">Team</span>
336
+ </a>`;
337
+ });
338
+
339
+ searchResults.innerHTML = html || '<div class="search-result">No results found</div>';
340
+ searchResults.classList.add('active');
341
+ } catch (e) {
342
+ console.error(e);
 
343
  }
 
344
  }
345
 
346
+ async function loadStatus() {
347
+ try {
348
+ const res = await fetch('/api/status');
349
+ const data = await res.json();
350
+
351
+ document.getElementById('statPosts').textContent = data.total_posts.toLocaleString();
352
+ document.getElementById('statMentions').textContent = data.total_mentions.toLocaleString();
353
+ document.getElementById('statPlayers').textContent = data.unique_players;
354
+
355
+ if (data.last_update) {
356
+ const d = new Date(data.last_update);
357
+ document.getElementById('statUpdated').textContent = d.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'});
358
+ }
359
+
360
+ const statusBar = document.getElementById('statusBar');
361
+ if (data.is_fetching) {
362
+ statusBar.className = 'status-bar fetching';
363
+ statusBar.textContent = 'πŸ”„ ' + data.fetch_status;
364
+ } else {
365
+ statusBar.className = 'status-bar';
366
+ statusBar.textContent = 'βœ“ Data loaded β€’ Click a player or team for details';
367
+ }
368
+ } catch (e) { console.error(e); }
369
  }
370
 
371
+ async function loadRankings() {
372
+ const list = document.getElementById('rankingsList');
373
+ list.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading...</p></div>';
 
374
 
375
+ const endpoint = currentTab === 'players' ? '/api/players' : '/api/teams';
376
+ document.getElementById('rankingsHeader').textContent = currentTab === 'players' ? 'Trending Players' : 'Trending Teams';
 
 
 
377
 
378
  try {
379
+ const res = await fetch(`${endpoint}?hours=${hours}`);
380
+ const data = await res.json();
381
+
382
+ if (data.length === 0) {
383
+ list.innerHTML = '<div class="empty-state"><h3>No data yet</h3><p>Click Refresh to load data</p></div>';
384
+ return;
385
+ }
386
+
387
+ list.innerHTML = data.map(item => {
388
+ const name = item.player || item.team;
389
+ const team = currentTab === 'players' ? (item.team || '') : '';
390
+ const url = currentTab === 'players' ? `/player/${encodeURIComponent(name)}` : `/team/${encodeURIComponent(name)}`;
391
+
392
  return `
393
+ <a href="${url}" class="ranking-item">
394
+ <div class="rank-num">${item.rank}</div>
395
+ <div class="player-info">
396
+ <div class="player-name">${name}</div>
397
+ ${team ? `<div class="player-team">${team}</div>` : ''}
 
 
 
398
  </div>
399
+ <div class="mention-count">
400
+ <div class="mention-num">${item.mentions}</div>
401
+ <div class="mention-label">mentions</div>
402
+ </div>
403
+ </a>
404
+ `;
405
+ }).join('');
406
+ } catch (e) {
407
+ list.innerHTML = '<div class="empty-state"><h3>Error loading data</h3></div>';
408
+ }
409
+ }
410
+
411
+ document.querySelectorAll('.tab-btn').forEach(btn => {
412
+ btn.addEventListener('click', () => {
413
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
414
+ btn.classList.add('active');
415
+ currentTab = btn.dataset.tab;
416
+ loadRankings();
417
+ });
418
+ });
419
+
420
+ document.querySelectorAll('.time-btn').forEach(btn => {
421
+ btn.addEventListener('click', () => {
422
+ document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
423
+ btn.classList.add('active');
424
+ hours = parseInt(btn.dataset.hours);
425
+ loadRankings();
426
+ });
427
+ });
428
+
429
+ document.getElementById('refreshBtn').addEventListener('click', async () => {
430
+ const btn = document.getElementById('refreshBtn');
431
+ btn.disabled = true;
432
+ btn.textContent = '⏳ Refreshing...';
433
+
434
+ await fetch('/api/refresh', { method: 'POST' });
435
+
436
+ const check = async () => {
437
+ const res = await fetch('/api/status');
438
+ const data = await res.json();
439
+ if (data.is_fetching) {
440
+ setTimeout(check, 2000);
441
+ } else {
442
+ btn.disabled = false;
443
+ btn.textContent = 'πŸ”„ Refresh';
444
+ loadStatus();
445
+ loadRankings();
446
+ }
447
+ };
448
+ setTimeout(check, 1000);
449
+ });
450
+
451
+ loadStatus();
452
+ loadRankings();
453
+ setInterval(loadStatus, 10000);
454
  </script>
455
  </body>
456
  </html>