TheContrast commited on
Commit
7f2753f
·
verified ·
1 Parent(s): 811707c

So, is this just the code to put somewhere, and that in turn would give me the app?

Browse files
components/loader.js ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class LoaderComponent extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.attachShadow({ mode: 'open' });
8
+ this.render();
9
+ }
10
+
11
+ render() {
12
+ this.shadowRoot.innerHTML = `
13
+ <style>
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ :host {
21
+ display: contents;
22
+ }
23
+
24
+ .loader-grid {
25
+ display: grid;
26
+ grid-template-columns: repeat(1, 1fr);
27
+ gap: 1.5rem;
28
+ }
29
+
30
+ @media (min-width: 768px) {
31
+ .loader-grid {
32
+ grid-template-columns: repeat(2, 1fr);
33
+ }
34
+ }
35
+
36
+ @media (min-width: 1024px) {
37
+ .loader-grid {
38
+ grid-template-columns: repeat(3, 1fr);
39
+ }
40
+ }
41
+
42
+ .skeleton-card {
43
+ background: white;
44
+ border-radius: 1rem;
45
+ overflow: hidden;
46
+ border: 1px solid #e5e7eb;
47
+ }
48
+
49
+ :host-context(.dark) .skeleton-card {
50
+ background: #1f2937;
51
+ border-color: #374151;
52
+ }
53
+
54
+ .skeleton-image {
55
+ height: 12rem;
56
+ background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
57
+ background-size: 200% 100%;
58
+ animation: shimmer 1.5s infinite;
59
+ }
60
+
61
+ :host-context(.dark) .skeleton-image {
62
+ background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
63
+ background-size: 200% 100%;
64
+ }
65
+
66
+ .skeleton-content {
67
+ padding: 1.25rem;
68
+ }
69
+
70
+ .skeleton-line {
71
+ height: 0.75rem;
72
+ background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
73
+ background-size: 200% 100%;
74
+ animation: shimmer 1.5s infinite;
75
+ border-radius: 0.25rem;
76
+ margin-bottom: 0.75rem;
77
+ }
78
+
79
+ :host-context(.dark) .skeleton-line {
80
+ background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
81
+ background-size: 200% 100%;
82
+ }
83
+
84
+ .skeleton-line.short {
85
+ width: 60%;
86
+ }
87
+
88
+ .skeleton-line.title {
89
+ height: 1.25rem;
90
+ margin-bottom: 1rem;
91
+ }
92
+
93
+ @keyframes shimmer {
94
+ 0% { background-position: 200% 0; }
95
+ 100% { background-position: -200% 0; }
96
+ }
97
+ </style>
98
+
99
+ <div class="loader-grid">
100
+ ${Array(6).fill(0).map(() => `
101
+ <div class="skeleton-card">
102
+ <div class="skeleton-image"></div>
103
+ <div class="skeleton-content">
104
+ <div class="skeleton-line short"></div>
105
+ <div class="skeleton-line title"></div>
106
+ <div class="skeleton-line"></div>
107
+ <div class="skeleton-line short"></div>
108
+ </div>
109
+ </div>
110
+ `).join('')}
111
+ </div>
112
+ `;
113
+ }
114
+ }
115
+
116
+ customElements.define('loader-component', LoaderComponent);
components/news-card.js ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class NewsCard extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.attachShadow({ mode: 'open' });
8
+ this.render();
9
+ }
10
+
11
+ static get observedAttributes() {
12
+ return ['title', 'url', 'permalink', 'author', 'score', 'comments', 'created', 'thumbnail', 'category', 'priority'];
13
+ }
14
+
15
+ attributeChangedCallback() {
16
+ this.render();
17
+ }
18
+
19
+ render() {
20
+ const title = this.getAttribute('title') || '';
21
+ const url = this.getAttribute('url') || '';
22
+ const permalink = this.getAttribute('permalink') || '';
23
+ const author = this.getAttribute('author') || '';
24
+ const score = this.getAttribute('score') || '0';
25
+ const comments = this.getAttribute('comments') || '0';
26
+ const created = this.getAttribute('created') || new Date().toISOString();
27
+ const thumbnail = this.getAttribute('thumbnail') || '';
28
+ const category = this.getAttribute('category') || 'general';
29
+ const priority = this.getAttribute('priority') || 'low';
30
+
31
+ const categoryColors = {
32
+ ai: 'from-primary-500 to-secondary-500',
33
+ science: 'from-emerald-500 to-teal-500',
34
+ tech: 'from-amber-500 to-orange-500',
35
+ nhl: 'from-orange-500 to-blue-600',
36
+ general: 'from-gray-500 to-gray-600'
37
+ };
38
+
39
+ const tagColors = {
40
+ ai: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
41
+ science: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
42
+ tech: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
43
+ nhl: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
44
+ general: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
45
+ };
46
+
47
+ const timeAgo = this.getTimeAgo(new Date(created));
48
+ const hasImage = thumbnail && thumbnail.startsWith('http');
49
+
50
+ this.shadowRoot.innerHTML = `
51
+ <style>
52
+ * {
53
+ margin: 0;
54
+ padding: 0;
55
+ box-sizing: border-box;
56
+ }
57
+
58
+ :host {
59
+ display: block;
60
+ }
61
+
62
+ .card {
63
+ background: white;
64
+ border-radius: 1rem;
65
+ overflow: hidden;
66
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
67
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
68
+ border: 1px solid #e5e7eb;
69
+ cursor: pointer;
70
+ }
71
+
72
+ :host-context(.dark) .card {
73
+ background: #1f2937;
74
+ border-color: #374151;
75
+ }
76
+
77
+ .card:hover {
78
+ transform: translateY(-4px);
79
+ box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.25);
80
+ }
81
+
82
+ .image-container {
83
+ position: relative;
84
+ height: 12rem;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .image-container img {
89
+ width: 100%;
90
+ height: 100%;
91
+ object-fit: cover;
92
+ transition: transform 0.5s ease;
93
+ }
94
+
95
+ .card:hover .image-container img {
96
+ transform: scale(1.1);
97
+ }
98
+
99
+ .gradient-bg {
100
+ height: 8rem;
101
+ background: linear-gradient(135deg, var(--gradient-from, #0ea5e9), var(--gradient-to, #8b5cf6));
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ }
106
+
107
+ .gradient-bg svg {
108
+ width: 4rem;
109
+ height: 4rem;
110
+ color: rgba(255,255,255,0.3);
111
+ }
112
+
113
+ .tag {
114
+ position: absolute;
115
+ top: 0.75rem;
116
+ left: 0.75rem;
117
+ padding: 0.25rem 0.5rem;
118
+ border-radius: 0.5rem;
119
+ font-size: 0.75rem;
120
+ font-weight: 600;
121
+ text-transform: uppercase;
122
+ backdrop-filter: blur(4px);
123
+ }
124
+
125
+ .priority-indicator {
126
+ position: absolute;
127
+ top: 0.75rem;
128
+ right: 0.75rem;
129
+ width: 0.5rem;
130
+ height: 0.5rem;
131
+ background: #ef4444;
132
+ border-radius: 50%;
133
+ animation: pulse 2s ease-in-out infinite;
134
+ }
135
+
136
+ @keyframes pulse {
137
+ 0%, 100% { opacity: 1; transform: scale(1); }
138
+ 50% { opacity: 0.5; transform: scale(1.2); }
139
+ }
140
+
141
+ .content {
142
+ padding: 1.25rem;
143
+ }
144
+
145
+ .meta {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 0.5rem;
149
+ font-size: 0.75rem;
150
+ color: #6b7280;
151
+ margin-bottom: 0.75rem;
152
+ }
153
+
154
+ :host-context(.dark) .meta {
155
+ color: #9ca3af;
156
+ }
157
+
158
+ .meta img {
159
+ width: 1.25rem;
160
+ height: 1.25rem;
161
+ border-radius: 50%;
162
+ }
163
+
164
+ .title {
165
+ font-size: 1rem;
166
+ font-weight: 600;
167
+ color: #111827;
168
+ line-height: 1.5;
169
+ margin-bottom: 1rem;
170
+ display: -webkit-box;
171
+ -webkit-line-clamp: 2;
172
+ -webkit-box-orient: vertical;
173
+ overflow: hidden;
174
+ transition: color 0.2s ease;
175
+ }
176
+
177
+ :host-context(.dark) .title {
178
+ color: #f9fafb;
179
+ }
180
+
181
+ .card:hover .title {
182
+ color: #0ea5e9;
183
+ }
184
+
185
+ :host-context(.dark) .card:hover .title {
186
+ color: #38bdf8;
187
+ }
188
+
189
+ .footer {
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ }
194
+
195
+ .stats {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 0.75rem;
199
+ font-size: 0.875rem;
200
+ color: #6b7280;
201
+ }
202
+
203
+ :host-context(.dark) .stats {
204
+ color: #9ca3af;
205
+ }
206
+
207
+ .stat {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 0.25rem;
211
+ }
212
+
213
+ .stat svg {
214
+ width: 1rem;
215
+ height: 1rem;
216
+ }
217
+
218
+ .save-btn {
219
+ padding: 0.5rem;
220
+ border-radius: 0.5rem;
221
+ border: none;
222
+ background: transparent;
223
+ cursor: pointer;
224
+ color: #6b7280;
225
+ transition: all 0.2s ease;
226
+ }
227
+
228
+ :host-context(.dark) .save-btn {
229
+ color: #9ca3af;
230
+ }
231
+
232
+ .save-btn:hover {
233
+ background: #f3f4f6;
234
+ color: #0ea5e9;
235
+ }
236
+
237
+ :host-context(.dark) .save-btn:hover {
238
+ background: #374151;
239
+ color: #38bdf8;
240
+ }
241
+
242
+ .overlay {
243
+ position: absolute;
244
+ inset: 0;
245
+ background: linear-gradient(to top, rgba(0,0,0,0.6), transparent 50%);
246
+ opacity: 0;
247
+ transition: opacity 0.3s ease;
248
+ }
249
+
250
+ .card:hover .overlay {
251
+ opacity: 1;
252
+ }
253
+ </style>
254
+
255
+ <article class="card" onclick="window.open('${url && !url.startsWith('#') ? url : permalink}', '_blank')">
256
+ ${hasImage ? `
257
+ <div class="image-container">
258
+ <img src="${thumbnail}" alt="" loading="lazy">
259
+ <div class="overlay"></div>
260
+ <span class="tag ${tagColors[category] || tagColors.general}">${category}</span>
261
+ ${priority === 'high' ? '<span class="priority-indicator"></span>' : ''}
262
+ </div>
263
+ ` : `
264
+ <div class="gradient-bg" style="--gradient-from: ${this.getGradientColors(category).from}; --gradient-to: ${this.getGradientColors(category).to}">
265
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${this.getIconPath(category)}</svg>
266
+ <span class="tag ${tagColors[category] || tagColors.general}" style="position: absolute; top: 0.75rem; left: 0.75rem;">${category}</span>
267
+ </div>
268
+ `}
269
+
270
+ <div class="content">
271
+ <div class="meta">
272
+ <img src="https://static.photos/minimal/32x32/${author.charCodeAt(0) || 65}" alt="">
273
+ <span>${author}</span>
274
+ <span>•</span>
275
+ <span>${timeAgo}</span>
276
+ </div>
277
+
278
+ <h3 class="title">${this.escapeHtml(title)}</h3>
279
+
280
+ <div class="footer">
281
+ <div class="stats">
282
+ <span class="stat">
283
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
284
+ ${this.formatNumber(parseInt(score))}
285
+ </span>
286
+ <span class="stat">
287
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
288
+ ${comments}
289
+ </span>
290
+ </div>
291
+ <button class="save-btn" onclick="event.stopPropagation(); window.saveArticle && window.saveArticle('${this.getAttribute('id') || ''}')">
292
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
293
+ </button>
294
+ </div>
295
+ </div>
296
+ </article>
297
+ `;
298
+ }
299
+
300
+ getGradientColors(category) {
301
+ const colors = {
302
+ ai: { from: '#0ea5e9', to: '#8b5cf6' },
303
+ science: { from: '#10b981', to: '#14b8a6' },
304
+ tech: { from: '#f59e0b', to: '#f97316' },
305
+ nhl: { from: '#f47920', to: '#041e42' },
306
+ general: { from: '#6b7280', to: '#4b5563' }
307
+ };
308
+ return colors[category] || colors.general;
309
+ }
310
+
311
+ getIconPath(category) {
312
+ const icons = {
313
+ ai: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path>',
314
+ science: '<circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>',
315
+ tech: '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line>',
316
+ nhl: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>',
317
+ general: '<circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>'
318
+ };
319
+ return icons[category] || icons.general;
320
+ }
321
+
322
+ getTimeAgo(date) {
323
+ const seconds = Math.floor((new Date() - date) / 1000);
324
+ const intervals = {
325
+ year: 31536000,
326
+ month: 2592000,
327
+ week: 604800,
328
+ day: 86400,
329
+ hour: 3600,
330
+ minute: 60
331
+ };
332
+ for (const [unit, secondsInUnit] of Object.entries(intervals)) {
333
+ const interval = Math.floor(seconds / secondsInUnit);
334
+ if (interval >= 1) {
335
+ return `${interval} ${unit}${interval > 1 ? 's' : ''} ago`;
336
+ }
337
+ }
338
+ return 'just now';
339
+ }
340
+
341
+ formatNumber(num) {
342
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
343
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
344
+ return num.toString();
345
+ }
346
+
347
+ escapeHtml(text) {
348
+ const div = document.createElement('div');
349
+ div.textContent = text;
350
+ return div.innerHTML;
351
+ }
352
+ }
353
+
354
+ customElements.define('news-card', NewsCard);
components/sidebar.js ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class SideBar extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.attachShadow({ mode: 'open' });
8
+ this.render();
9
+ this.setupEvents();
10
+ }
11
+
12
+ render() {
13
+ const dark = document.documentElement.classList.contains('dark');
14
+
15
+ this.shadowRoot.innerHTML = `
16
+ <style>
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ :host {
24
+ display: block;
25
+ }
26
+
27
+ .sidebar {
28
+ position: fixed;
29
+ top: 64px;
30
+ left: 0;
31
+ bottom: 0;
32
+ width: 256px;
33
+ background: rgba(255, 255, 255, 0.95);
34
+ backdrop-filter: blur(12px);
35
+ border-right: 1px solid rgba(229, 231, 235, 0.5);
36
+ padding: 1.5rem 1rem;
37
+ overflow-y: auto;
38
+ z-index: 40;
39
+ transform: translateX(-100%);
40
+ transition: transform 0.3s ease;
41
+ }
42
+
43
+ .sidebar.open {
44
+ transform: translateX(0);
45
+ }
46
+
47
+ @media (min-width: 1024px) {
48
+ .sidebar {
49
+ transform: translateX(0);
50
+ }
51
+ }
52
+
53
+ :host-context(.dark) .sidebar,
54
+ .sidebar.dark {
55
+ background: rgba(15, 23, 42, 0.95);
56
+ border-right-color: rgba(75, 85, 99, 0.5);
57
+ }
58
+
59
+ .section {
60
+ margin-bottom: 1.5rem;
61
+ }
62
+
63
+ .section-title {
64
+ font-size: 0.75rem;
65
+ font-weight: 600;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.05em;
68
+ color: #9ca3af;
69
+ margin-bottom: 0.75rem;
70
+ padding-left: 0.75rem;
71
+ }
72
+
73
+ :host-context(.dark) .section-title,
74
+ .sidebar.dark .section-title {
75
+ color: #6b7280;
76
+ }
77
+
78
+ .nav-item {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 0.75rem;
82
+ padding: 0.625rem 0.75rem;
83
+ border-radius: 0.75rem;
84
+ text-decoration: none;
85
+ color: #374151;
86
+ font-size: 0.875rem;
87
+ font-weight: 500;
88
+ transition: all 0.2s ease;
89
+ margin-bottom: 0.25rem;
90
+ }
91
+
92
+ :host-context(.dark) .nav-item,
93
+ .sidebar.dark .nav-item {
94
+ color: #d1d5db;
95
+ }
96
+
97
+ .nav-item:hover {
98
+ background: #f3f4f6;
99
+ color: #0ea5e9;
100
+ }
101
+
102
+ :host-context(.dark) .nav-item:hover,
103
+ .sidebar.dark .nav-item:hover {
104
+ background: #374151;
105
+ color: #38bdf8;
106
+ }
107
+
108
+ .nav-item.active {
109
+ background: linear-gradient(135deg, #0ea5e9, #8b5cf6);
110
+ color: white;
111
+ }
112
+
113
+ .nav-icon {
114
+ width: 20px;
115
+ height: 20px;
116
+ flex-shrink: 0;
117
+ }
118
+
119
+ .stats-card {
120
+ background: linear-gradient(135deg, #0ea5e9, #8b5cf6);
121
+ border-radius: 1rem;
122
+ padding: 1rem;
123
+ color: white;
124
+ margin-top: 1rem;
125
+ }
126
+
127
+ .stats-header {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 0.5rem;
131
+ font-size: 0.75rem;
132
+ font-weight: 600;
133
+ text-transform: uppercase;
134
+ opacity: 0.9;
135
+ margin-bottom: 0.75rem;
136
+ }
137
+
138
+ .stat-row {
139
+ display: flex;
140
+ justify-content: space-between;
141
+ align-items: center;
142
+ padding: 0.5rem 0;
143
+ border-bottom: 1px solid rgba(255,255,255,0.2);
144
+ }
145
+
146
+ .stat-row:last-child {
147
+ border-bottom: none;
148
+ }
149
+
150
+ .stat-label {
151
+ font-size: 0.875rem;
152
+ opacity: 0.9;
153
+ }
154
+
155
+ .stat-value {
156
+ font-size: 1.125rem;
157
+ font-weight: 700;
158
+ }
159
+
160
+ .live-indicator {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 0.5rem;
164
+ margin-top: 1rem;
165
+ padding: 0.75rem;
166
+ background: rgba(16, 185, 129, 0.1);
167
+ border-radius: 0.75rem;
168
+ }
169
+
170
+ .live-dot {
171
+ width: 8px;
172
+ height: 8px;
173
+ background: #10b981;
174
+ border-radius: 50%;
175
+ animation: pulse 2s ease-in-out infinite;
176
+ }
177
+
178
+ @keyframes pulse {
179
+ 0%, 100% { opacity: 1; transform: scale(1); }
180
+ 50% { opacity: 0.5; transform: scale(1.2); }
181
+ }
182
+
183
+ .live-text {
184
+ font-size: 0.875rem;
185
+ font-weight: 500;
186
+ color: #10b981;
187
+ }
188
+ </style>
189
+
190
+ <aside class="sidebar ${dark ? 'dark' : ''}" id="sidebar">
191
+ <div class="section">
192
+ <div class="section-title">Discover</div>
193
+ <a href="#ai-section" class="nav-item active" data-section="ai">
194
+ <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path></svg>
195
+ Artificial Intelligence
196
+ </a>
197
+ <a href="#science-section" class="nav-item" data-section="science">
198
+ <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
199
+ Science & Space
200
+ </a>
201
+ <a href="#tech-section" class="nav-item" data-section="tech">
202
+ <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
203
+ Technology
204
+ </a>
205
+ <a href="#nhl-section" class="nav-item" data-section="nhl" style="color: #f47920;">
206
+ <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
207
+ NHL & McDavid
208
+ </a>
209
+ </div>
210
+
211
+ <div class="section">
212
+ <div class="section-title">Quick Stats</div>
213
+ <div class="stats-card">
214
+ <div class="stats-header">
215
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
216
+ Live Updates
217
+ </div>
218
+ <div class="stat-row">
219
+ <span class="stat-label">AI Articles</span>
220
+ <span class="stat-value" id="sidebar-ai-count">0</span>
221
+ </div>
222
+ <div class="stat-row">
223
+ <span class="stat-label">McDavid PTS</span>
224
+ <span class="stat-value" id="sidebar-mcdavid-pts">--</span>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ <div class="live-indicator">
230
+ <span class="live-dot"></span>
231
+ <span class="live-text">Live Data Feed</span>
232
+ </div>
233
+ </aside>
234
+ `;
235
+ }
236
+
237
+ setupEvents() {
238
+ const sidebar = this.shadowRoot.getElementById('sidebar');
239
+ const navItems = this.shadowRoot.querySelectorAll('.nav-item');
240
+
241
+ // Toggle sidebar on mobile
242
+ window.addEventListener('toggleSidebar', () => {
243
+ sidebar.classList.toggle('open');
244
+ });
245
+
246
+ // Update active state on scroll
247
+ const sections = ['ai-section', 'science-section', 'tech-section', 'nhl-section'];
248
+ const observer = new IntersectionObserver((entries) => {
249
+ entries.forEach(entry => {
250
+ if (entry.isIntersecting) {
251
+ const section = entry.target.id.replace('-section', '');
252
+ navItems.forEach(item => {
253
+ item.classList.toggle('active', item.dataset.section === section);
254
+ });
255
+ }
256
+ });
257
+ }, { threshold: 0.3 });
258
+
259
+ sections.forEach(id => {
260
+ const el = document.getElementById(id);
261
+ if (el) observer.observe(el);
262
+ });
263
+
264
+ // Close sidebar on mobile when clicking a link
265
+ navItems.forEach(item => {
266
+ item.addEventListener('click', () => {
267
+ if (window.innerWidth < 1024) {
268
+ sidebar.classList.remove('open');
269
+ }
270
+ });
271
+ });
272
+
273
+ // Listen for theme changes
274
+ window.addEventListener('themechange', () => {
275
+ const dark = document.documentElement.classList.contains('dark');
276
+ sidebar.classList.toggle('dark', dark);
277
+ });
278
+
279
+ // Listen for stats updates
280
+ window.addEventListener('statsupdate', (e) => {
281
+ const aiCount = this.shadowRoot.getElementById('sidebar-ai-count');
282
+ const mcdavidPts = this.shadowRoot.getElementById('sidebar-mcdavid-pts');
283
+ if (aiCount && e.detail.aiCount !== undefined) aiCount.textContent = e.detail.aiCount;
284
+ if (mcdavidPts && e.detail.mcdavidPoints !== undefined) mcdavidPts.textContent = e.detail.mcdavidPoints;
285
+ });
286
+ }
287
+ }
288
+
289
+ customElements.define('side-bar', SideBar);
components/topic-filter.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TopicFilter extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.attachShadow({ mode: 'open' });
8
+ this.render();
9
+ this.setupEvents();
10
+ }
11
+
12
+ render() {
13
+ const dark = document.documentElement.classList.contains('dark');
14
+
15
+ this.shadowRoot.innerHTML = `
16
+ <style>
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ :host {
24
+ display: block;
25
+ }
26
+
27
+ .filter-container {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 0.5rem;
31
+ overflow-x: auto;
32
+ padding: 0.25rem;
33
+ scrollbar-width: none;
34
+ -ms-overflow-style: none;
35
+ }
36
+
37
+ .filter-container::-webkit-scrollbar {
38
+ display: none;
39
+ }
40
+
41
+ .filter-btn {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 0.5rem;
45
+ padding: 0.5rem 1rem;
46
+ border-radius: 9999px;
47
+ border: 1px solid #e5e7eb;
48
+ background: white;
49
+ color: #374151;
50
+ font-size: 0.875rem;
51
+ font-weight: 500;
52
+ cursor: pointer;
53
+ transition: all 0.2s ease;
54
+ white-space: nowrap;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ :host-context(.dark) .filter-btn {
59
+ background: #1f2937;
60
+ border-color: #374151;
61
+ color: #d1d5db;
62
+ }
63
+
64
+ .filter-btn:hover {
65
+ border-color: #0ea5e9;
66
+ color: #0ea5e9;
67
+ }
68
+
69
+ .filter-btn.active {
70
+ background: linear-gradient(135deg, #0ea5e9, #8b5cf6);
71
+ border-color: transparent;
72
+ color: white;
73
+ transform: scale(1.05);
74
+ }
75
+
76
+ .filter-btn svg {
77
+ width: 16px;
78
+ height: 16px;
79
+ }
80
+
81
+ .filter-label {
82
+ font-size: 0.875rem;
83
+ font-weight: 600;
84
+ color: #6b7280;
85
+ margin-right: 0.5rem;
86
+ flex-shrink: 0;
87
+ }
88
+
89
+ :host-context(.dark) .filter-label {
90
+ color: #9ca3af;
91
+ }
92
+ </style>
93
+
94
+ <div class="filter-container">
95
+ <span class="filter-label">Filter:</span>
96
+ <button class="filter-btn active" data-filter="all">
97
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
98
+ All Topics
99
+ </button>
100
+ <button class="filter-btn" data-filter="ai">
101
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path></svg>
102
+ AI
103
+ </button>
104
+ <button class="filter-btn" data-filter="science">
105
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
106
+ Science
107
+ </button>
108
+ <button class="filter-btn" data-filter="tech">
109
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
110
+ Tech
111
+ </button>
112
+ <button class="filter-btn" data-filter="nhl" style="color: #f47920;">
113
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
114
+ NHL
115
+ </button>
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ setupEvents() {
121
+ const buttons = this.shadowRoot.querySelectorAll('.filter-btn');
122
+
123
+ buttons.forEach(btn => {
124
+ btn.addEventListener('click', () => {
125
+ buttons.forEach(b => b.classList.remove('active'));
126
+ btn.classList.add('active');
127
+
128
+ const filter = btn.dataset.filter;
129
+ window.dispatchEvent(new CustomEvent('filterchange', { detail: { filter } }));
130
+ });
131
+ });
132
+ }
133
+ }
134
+
135
+ customElements.define('topic-filter', TopicFilter);
script.js CHANGED
@@ -533,7 +533,6 @@ function showToast(message) {
533
  document.body.appendChild(toast);
534
  setTimeout(() => toast.remove(), 3000);
535
  }
536
-
537
  function updateLastUpdated() {
538
  const el = document.getElementById('last-updated');
539
  if (el && state.lastUpdated) {
@@ -545,8 +544,15 @@ function updateLastUpdated() {
545
  if (aiCount) {
546
  aiCount.textContent = state.aiArticles.length;
547
  }
 
 
 
 
 
 
 
 
548
  }
549
-
550
  // Fallback Data
551
  function loadFallbackData() {
552
  const fallbackAI = [
 
533
  document.body.appendChild(toast);
534
  setTimeout(() => toast.remove(), 3000);
535
  }
 
536
  function updateLastUpdated() {
537
  const el = document.getElementById('last-updated');
538
  if (el && state.lastUpdated) {
 
544
  if (aiCount) {
545
  aiCount.textContent = state.aiArticles.length;
546
  }
547
+
548
+ // Dispatch stats update event for sidebar
549
+ window.dispatchEvent(new CustomEvent('statsupdate', {
550
+ detail: {
551
+ aiCount: state.aiArticles.length,
552
+ mcdavidPoints: state.mcdavidStats?.points || '--'
553
+ }
554
+ }));
555
  }
 
556
  // Fallback Data
557
  function loadFallbackData() {
558
  const fallbackAI = [