cdechoch commited on
Commit
3623ae0
·
verified ·
1 Parent(s): 26e4b84

Update team.html

Browse files
Files changed (1) hide show
  1. team.html +141 -404
team.html CHANGED
@@ -3,454 +3,191 @@
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>
 
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; margin-bottom: 20px; }
26
+ .period-stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-top: 15px; }
27
+ .period-stat { background: #f8f9fa; border-radius: 8px; padding: 12px; text-align: center; }
28
+ .period-label { font-size: 12px; color: #888; font-weight: 600; margin-bottom: 4px; }
29
+ .period-mentions { font-size: 20px; font-weight: bold; color: #f97316; }
30
+ .period-rank { font-size: 11px; color: #666; margin-top: 2px; }
31
+ .section-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #f97316; }
32
+ .mentions-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 30px; }
33
+ .mention-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
34
+ .mention-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
35
+ .author-avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; background: #f0f0f0; }
36
+ .author-info { flex: 1; }
37
+ .author-name { font-weight: 600; font-size: 15px; color: #333; text-decoration: none; display: block; }
38
+ .author-name:hover { color: #f97316; }
39
+ .author-handle { font-size: 13px; color: #888; text-decoration: none; }
40
+ .author-handle:hover { color: #f97316; }
41
+ .mention-time { font-size: 12px; color: #aaa; }
42
+ .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; }
43
+ .player-tag:hover { background: #ea580c; }
44
+ .mention-text { font-size: 15px; line-height: 1.6; color: #333; margin-bottom: 12px; white-space: pre-wrap; word-wrap: break-word; }
45
+ .mention-text a { color: #f97316; text-decoration: none; }
46
+ .mention-text a:hover { text-decoration: underline; }
47
+ .mention-text .player-link { color: #2563eb; font-weight: 500; }
48
+ .mention-footer { margin-top: 12px; }
49
+ .view-original { color: #f97316; text-decoration: none; font-size: 14px; font-weight: 500; }
50
+ .view-original:hover { text-decoration: underline; }
51
+ .related-section { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
52
+ .related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
53
+ .related-link { display: block; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; text-decoration: none; color: #333; font-weight: 500; }
54
+ .related-link:hover { background: #fff8f3; color: #f97316; }
55
+ .bottom-nav { text-align: center; padding: 20px; border-top: 1px solid #eee; margin-top: 20px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  .loading { text-align: center; padding: 40px; color: #888; }
57
+ .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; }
 
 
 
 
 
 
 
 
58
  @keyframes spin { to { transform: rotate(360deg); } }
59
+ .empty-state { text-align: center; padding: 40px; background: white; border-radius: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  </style>
61
  </head>
62
  <body>
63
  <div class="container">
64
+ <a href="/" class="back-link">← Back to Rankings</a>
 
 
 
 
65
  <div class="search-container">
66
  <input type="text" class="search-input" id="searchInput" placeholder="Search players or teams...">
67
  <div class="search-results" id="searchResults"></div>
68
  </div>
69
+ <div class="team-header">
70
+ <div class="team-name" id="teamName">Loading...</div>
71
+ <div class="period-stats" id="periodStats">
72
+ <div class="period-stat"><div class="period-label">6h</div><div class="period-mentions">-</div><div class="period-rank"></div></div>
73
+ <div class="period-stat"><div class="period-label">12h</div><div class="period-mentions">-</div><div class="period-rank"></div></div>
74
+ <div class="period-stat"><div class="period-label">24h</div><div class="period-mentions">-</div><div class="period-rank"></div></div>
75
+ <div class="period-stat"><div class="period-label">48h</div><div class="period-mentions">-</div><div class="period-rank"></div></div>
76
+ <div class="period-stat"><div class="period-label">7d</div><div class="period-mentions">-</div><div class="period-rank"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  </div>
78
  </div>
79
+ <div class="section-title">Recent Mentions</div>
80
+ <div class="mentions-list" id="mentionsList"><div class="loading"><div class="spinner"></div><p>Loading...</p></div></div>
81
+ <div class="related-section">
82
+ <div class="section-title" style="border:none;margin:0 0 15px 0;padding:0;">Roster</div>
83
+ <div class="related-grid" id="rosterGrid"><div class="loading">Loading...</div></div>
84
  </div>
85
+ <div class="bottom-nav"><a href="/" class="back-link">← Back to Rankings</a></div>
86
  </div>
 
87
  <script>
88
+ const teamName = decodeURIComponent(window.location.pathname.split('/team/')[1] || '');
89
+ let allPlayers = [];
90
+
91
+ async function loadPlayerList() {
92
+ try { const res = await fetch('/api/players?hours=72&limit=200'); allPlayers = (await res.json()).map(p => p.player); } catch (e) {}
93
+ }
94
+ loadPlayerList();
95
 
 
96
  const searchInput = document.getElementById('searchInput');
97
  const searchResults = document.getElementById('searchResults');
98
+ let searchTimeout;
99
  searchInput.addEventListener('input', (e) => {
100
  clearTimeout(searchTimeout);
101
  const q = e.target.value.trim();
102
+ if (q.length < 2) { searchResults.classList.remove('active'); return; }
 
 
 
103
  searchTimeout = setTimeout(() => doSearch(q), 200);
104
  });
105
+ document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) searchResults.classList.remove('active'); });
 
 
 
 
 
 
 
 
 
106
 
107
  async function doSearch(q) {
108
+ const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
109
+ const data = await res.json();
110
+ let html = '';
111
+ data.players.forEach(p => { html += `<a href="/player/${encodeURIComponent(p)}" class="search-result"><span class="name">${p}</span><span class="type">Player</span></a>`; });
112
+ data.teams.forEach(t => { html += `<a href="/team/${encodeURIComponent(t)}" class="search-result"><span class="name">${t}</span><span class="type">Team</span></a>`; });
113
+ searchResults.innerHTML = html || '<div class="search-result">No results</div>';
114
+ searchResults.classList.add('active');
115
+ }
116
+
117
+ function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
118
+
119
+ function processText(text) {
120
+ if (!text) return '';
121
+ let escaped = escapeHtml(text);
122
+ // Match URLs - https://, http://, and common short domains
123
+ escaped = escaped.replace(/(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="$1" target="_blank">$1</a>');
124
+ // Match youtu.be, bit.ly, etc without protocol
125
+ 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>');
126
+ // Match www. URLs
127
+ escaped = escaped.replace(/(?<![\/\w])(www\.[^\s<]+[^<.,:;"')\]\s])/gi, '<a href="https://$1" target="_blank">$1</a>');
128
+ for (const player of allPlayers) {
129
+ const regex = new RegExp(`\\b(${player.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\b`, 'gi');
130
+ escaped = escaped.replace(regex, `<a href="/player/${encodeURIComponent(player)}" class="player-link">$1</a>`);
131
  }
132
+ return escaped;
133
  }
134
 
135
+ function timeAgo(d) {
136
+ if (!d) return '';
137
+ const s = Math.floor((new Date() - new Date(d)) / 1000);
138
+ if (s < 60) return 'just now';
139
+ if (s < 3600) return Math.floor(s/60) + 'm ago';
140
+ if (s < 86400) return Math.floor(s/3600) + 'h ago';
141
+ return Math.floor(s/86400) + 'd ago';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  }
143
 
144
+ async function loadTeam() {
145
+ if (!teamName) { document.getElementById('teamName').textContent = 'Not found'; return; }
146
+ document.getElementById('teamName').textContent = teamName;
147
+ document.title = teamName + ' - NBA Buzz';
 
 
148
 
149
  try {
150
+ const info = await (await fetch(`/api/team/${encodeURIComponent(teamName)}`)).json();
151
+ if (info.players?.length) document.getElementById('rosterGrid').innerHTML = info.players.map(p => `<a href="/player/${encodeURIComponent(p)}" class="related-link">${p}</a>`).join('');
152
+ else document.getElementById('rosterGrid').innerHTML = '<p style="color:#888">No players</p>';
153
 
154
+ // Populate period stats
155
+ if (info.period_stats) {
156
+ document.getElementById('periodStats').innerHTML = info.period_stats.map(p => `
157
+ <div class="period-stat">
158
+ <div class="period-label">${p.label}</div>
159
+ <div class="period-mentions">${p.mentions}</div>
160
+ <div class="period-rank">${p.rank ? '#' + p.rank : '-'}</div>
161
+ </div>
162
+ `).join('');
163
  }
164
+ } catch (e) { console.error(e); }
165
+
166
+ try {
167
+ const mentions = await (await fetch(`/api/team-mentions/${encodeURIComponent(teamName)}?limit=50`)).json();
168
+ if (!mentions.length) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>No mentions found</h3></div>'; return; }
169
+ document.getElementById('mentionsList').innerHTML = mentions.map(m => {
170
+ const avatarUrl = m.author_avatar || 'https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg';
171
  return `
172
+ <div class="mention-card">
173
+ <div class="mention-header">
174
+ <a href="https://bsky.app/profile/${m.author_handle}" target="_blank">
175
+ <img src="${avatarUrl}" class="author-avatar" onerror="this.src='https://cdn.bsky.app/img/avatar/plain/did:plc:default/default@jpeg'">
176
+ </a>
177
+ <div class="author-info">
178
+ <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-name">${escapeHtml(m.author)}</a>
179
+ <a href="https://bsky.app/profile/${m.author_handle}" target="_blank" class="author-handle">@${escapeHtml(m.author_handle?.split('.')[0] || '')}</a>
 
180
  </div>
181
+ <span class="mention-time">${timeAgo(m.created_at)}</span>
182
+ </div>
183
+ ${m.player ? `<a href="/player/${encodeURIComponent(m.player)}" class="player-tag">${escapeHtml(m.player)}</a>` : ''}
184
+ <div class="mention-text">${processText(m.text)}</div>
185
+ <div class="mention-footer"><a href="${m.url}" target="_blank" class="view-original">View on Bluesky →</a></div>
186
+ </div>
187
+ `}).join('');
188
+ } catch (e) { document.getElementById('mentionsList').innerHTML = '<div class="empty-state"><h3>Error loading</h3></div>'; }
189
+ }
190
+ loadTeam();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </script>
192
  </body>
193
  </html>