helmo commited on
Commit
775ce64
·
verified ·
1 Parent(s): 6ed404b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +606 -19
index.html CHANGED
@@ -1,19 +1,606 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>RSS Feed Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .news-card {
11
+ transition: all 0.3s ease;
12
+ }
13
+ .news-card:hover {
14
+ transform: translateY(-5px);
15
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
16
+ }
17
+ .source-tab {
18
+ transition: all 0.2s ease;
19
+ }
20
+ .source-tab:hover {
21
+ background-color: rgba(59, 130, 246, 0.1);
22
+ }
23
+ .source-tab.active {
24
+ border-left: 4px solid #3b82f6;
25
+ background-color: rgba(59, 130, 246, 0.05);
26
+ }
27
+ .fade-in {
28
+ animation: fadeIn 0.5s ease-in-out;
29
+ }
30
+ @keyframes fadeIn {
31
+ from { opacity: 0; }
32
+ to { opacity: 1; }
33
+ }
34
+ </style>
35
+ </head>
36
+ <body class="bg-gray-50 min-h-screen">
37
+ <div class="container mx-auto px-4 py-8">
38
+ <!-- Header -->
39
+ <header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8">
40
+ <div class="mb-4 md:mb-0">
41
+ <h1 class="text-3xl font-bold text-gray-800">RSS Feed Dashboard</h1>
42
+ <p class="text-gray-600">Stay updated with your favorite news sources</p>
43
+ </div>
44
+
45
+ <!-- Add Feed Form -->
46
+ <div class="w-full md:w-auto">
47
+ <div class="flex flex-col md:flex-row gap-2">
48
+ <input type="text" id="feedUrl" placeholder="Enter RSS feed URL"
49
+ class="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 w-full md:w-64">
50
+ <button id="addFeedBtn"
51
+ class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition-colors">
52
+ <i class="fas fa-plus"></i> Add Feed
53
+ </button>
54
+ </div>
55
+ <p id="feedError" class="text-red-500 text-sm mt-1 hidden"></p>
56
+ </div>
57
+ </header>
58
+
59
+ <div class="flex flex-col lg:flex-row gap-6">
60
+ <!-- Sidebar with sources -->
61
+ <div class="w-full lg:w-1/4 bg-white rounded-xl shadow-sm p-4 h-fit sticky top-4">
62
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center gap-2">
63
+ <i class="fas fa-newspaper text-blue-500"></i> News Sources
64
+ </h2>
65
+
66
+ <div class="space-y-2" id="sourcesList">
67
+ <!-- Default sources will be added here -->
68
+ </div>
69
+
70
+ <div class="mt-6">
71
+ <h3 class="text-sm font-medium text-gray-500 uppercase tracking-wider mb-2">Popular Sources</h3>
72
+ <div class="space-y-1">
73
+ <button class="text-left w-full px-3 py-2 text-sm text-blue-500 hover:bg-blue-50 rounded-lg transition-colors add-default-feed" data-feed="http://feeds.bbci.co.uk/news/rss.xml">
74
+ <i class="fas fa-globe-europe mr-2"></i> BBC News
75
+ </button>
76
+ <button class="text-left w-full px-3 py-2 text-sm text-blue-500 hover:bg-blue-50 rounded-lg transition-colors add-default-feed" data-feed="https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml">
77
+ <i class="fas fa-newspaper mr-2"></i> New York Times
78
+ </button>
79
+ <button class="text-left w-full px-3 py-2 text-sm text-blue-500 hover:bg-blue-50 rounded-lg transition-colors add-default-feed" data-feed="https://www.theguardian.com/world/rss">
80
+ <i class="fas fa-shield-alt mr-2"></i> The Guardian
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Main content area -->
87
+ <div class="w-full lg:w-3/4">
88
+ <div class="bg-white rounded-xl shadow-sm p-6">
89
+ <div class="flex justify-between items-center mb-6">
90
+ <h2 class="text-xl font-semibold text-gray-800" id="currentSourceTitle">
91
+ <i class="fas fa-globe-americas text-blue-500 mr-2"></i> All News
92
+ </h2>
93
+ <div class="flex items-center gap-2">
94
+ <span class="text-sm text-gray-500" id="lastUpdated"></span>
95
+ <button id="refreshBtn" class="text-blue-500 hover:text-blue-700 transition-colors">
96
+ <i class="fas fa-sync-alt"></i>
97
+ </button>
98
+ </div>
99
+ </div>
100
+
101
+ <div id="loadingIndicator" class="flex justify-center items-center py-12">
102
+ <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
103
+ </div>
104
+
105
+ <div id="newsContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 hidden">
106
+ <!-- News cards will be added here -->
107
+ </div>
108
+
109
+ <div id="noFeedsMessage" class="text-center py-12 hidden">
110
+ <i class="fas fa-newspaper text-4xl text-gray-300 mb-4"></i>
111
+ <h3 class="text-xl font-medium text-gray-500">No feeds added yet</h3>
112
+ <p class="text-gray-400 mt-2">Add your first RSS feed to get started</p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <script>
120
+ document.addEventListener('DOMContentLoaded', function() {
121
+ // DOM elements
122
+ const feedUrlInput = document.getElementById('feedUrl');
123
+ const addFeedBtn = document.getElementById('addFeedBtn');
124
+ const feedError = document.getElementById('feedError');
125
+ const sourcesList = document.getElementById('sourcesList');
126
+ const newsContainer = document.getElementById('newsContainer');
127
+ const loadingIndicator = document.getElementById('loadingIndicator');
128
+ const noFeedsMessage = document.getElementById('noFeedsMessage');
129
+ const currentSourceTitle = document.getElementById('currentSourceTitle');
130
+ const lastUpdated = document.getElementById('lastUpdated');
131
+ const refreshBtn = document.getElementById('refreshBtn');
132
+
133
+ // State
134
+ let feeds = [];
135
+ let currentFeed = null;
136
+ let articles = [];
137
+
138
+ // Initialize with default feeds if none in localStorage
139
+ if (!localStorage.getItem('rssFeeds')) {
140
+ const defaultFeeds = [
141
+ { url: 'http://feeds.bbci.co.uk/news/rss.xml', name: 'BBC News', icon: 'globe-europe' },
142
+ { url: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', name: 'New York Times', icon: 'newspaper' },
143
+ { url: 'https://www.theguardian.com/world/rss', name: 'The Guardian', icon: 'microphone-alt' }
144
+ ];
145
+ localStorage.setItem('rssFeeds', JSON.stringify(defaultFeeds));
146
+ }
147
+
148
+ // Load feeds from localStorage
149
+ loadFeeds();
150
+
151
+ // Event listeners
152
+ addFeedBtn.addEventListener('click', addNewFeed);
153
+ feedUrlInput.addEventListener('keypress', function(e) {
154
+ if (e.key === 'Enter') addNewFeed();
155
+ });
156
+
157
+ // Add default feeds
158
+ document.querySelectorAll('.add-default-feed').forEach(btn => {
159
+ btn.addEventListener('click', function() {
160
+ feedUrlInput.value = this.dataset.feed;
161
+ addNewFeed();
162
+ });
163
+ });
164
+
165
+ refreshBtn.addEventListener('click', function() {
166
+ if (currentFeed) {
167
+ fetchFeed(currentFeed.url);
168
+ } else if (feeds.length > 0) {
169
+ fetchAllFeeds();
170
+ }
171
+ });
172
+
173
+ // Functions
174
+ function loadFeeds() {
175
+ const savedFeeds = localStorage.getItem('rssFeeds');
176
+ if (savedFeeds) {
177
+ feeds = JSON.parse(savedFeeds);
178
+ renderSourcesList();
179
+
180
+ if (feeds.length > 0) {
181
+ fetchAllFeeds();
182
+ } else {
183
+ showNoFeedsMessage();
184
+ }
185
+ }
186
+ }
187
+
188
+ function renderSourcesList() {
189
+ sourcesList.innerHTML = '';
190
+
191
+ // Add "All News" option
192
+ const allNewsTab = document.createElement('button');
193
+ allNewsTab.className = 'source-tab w-full text-left px-3 py-2 rounded-lg flex items-center gap-2 font-medium';
194
+ allNewsTab.innerHTML = `
195
+ <i class="fas fa-globe-americas text-gray-500"></i>
196
+ <span>All News</span>
197
+ <span class="ml-auto bg-gray-100 text-gray-600 text-xs px-2 py-1 rounded-full">${feeds.length}</span>
198
+ `;
199
+ allNewsTab.addEventListener('click', function() {
200
+ document.querySelectorAll('.source-tab').forEach(tab => tab.classList.remove('active'));
201
+ this.classList.add('active');
202
+ currentFeed = null;
203
+ currentSourceTitle.innerHTML = '<i class="fas fa-globe-americas text-blue-500 mr-2"></i> All News';
204
+ renderArticles(articles);
205
+ });
206
+
207
+ if (!currentFeed && feeds.length > 0) {
208
+ allNewsTab.classList.add('active');
209
+ }
210
+
211
+ sourcesList.appendChild(allNewsTab);
212
+
213
+ // Add individual feeds
214
+ feeds.forEach(feed => {
215
+ const feedTab = document.createElement('button');
216
+ feedTab.className = 'source-tab w-full text-left px-3 py-2 rounded-lg flex items-center gap-2';
217
+ if (currentFeed && currentFeed.url === feed.url) {
218
+ feedTab.classList.add('active');
219
+ }
220
+
221
+ feedTab.innerHTML = `
222
+ <i class="fas fa-${feed.icon || 'rss'} text-gray-500"></i>
223
+ <span class="truncate">${feed.name || 'Unnamed Feed'}</span>
224
+ <button class="ml-auto text-gray-400 hover:text-red-500 delete-feed" data-url="${feed.url}">
225
+ <i class="fas fa-times"></i>
226
+ </button>
227
+ `;
228
+
229
+ feedTab.addEventListener('click', function() {
230
+ document.querySelectorAll('.source-tab').forEach(tab => tab.classList.remove('active'));
231
+ this.classList.add('active');
232
+ currentFeed = feed;
233
+ currentSourceTitle.innerHTML = `
234
+ <i class="fas fa-${feed.icon || 'rss'} text-blue-500 mr-2"></i> ${feed.name || 'Unnamed Feed'}
235
+ `;
236
+ const filteredArticles = articles.filter(article => article.feedUrl === feed.url);
237
+ renderArticles(filteredArticles);
238
+ });
239
+
240
+ sourcesList.appendChild(feedTab);
241
+ });
242
+
243
+ // Add delete handlers
244
+ document.querySelectorAll('.delete-feed').forEach(btn => {
245
+ btn.addEventListener('click', function(e) {
246
+ e.stopPropagation();
247
+ const urlToDelete = this.dataset.url;
248
+ deleteFeed(urlToDelete);
249
+ });
250
+ });
251
+ }
252
+
253
+ function addNewFeed() {
254
+ const feedUrl = feedUrlInput.value.trim();
255
+
256
+ if (!feedUrl) {
257
+ showError('Please enter a feed URL');
258
+ return;
259
+ }
260
+
261
+ // Basic URL validation
262
+ if (!isValidUrl(feedUrl)) {
263
+ showError('Please enter a valid URL');
264
+ return;
265
+ }
266
+
267
+ // Check if feed already exists
268
+ if (feeds.some(feed => feed.url === feedUrl)) {
269
+ showError('This feed is already added');
270
+ return;
271
+ }
272
+
273
+ hideError();
274
+
275
+ // Add temporary feed with loading state
276
+ const tempFeed = { url: feedUrl, name: 'Loading...', icon: 'rss' };
277
+ feeds.push(tempFeed);
278
+ saveFeeds();
279
+ renderSourcesList();
280
+
281
+ // Try to fetch the feed to get its title
282
+ fetchFeed(feedUrl, true)
283
+ .then(data => {
284
+ // Update feed name if we can extract it
285
+ const feedName = data.feed.title || new URL(feedUrl).hostname.replace('www.', '');
286
+ const updatedFeed = {
287
+ url: feedUrl,
288
+ name: feedName,
289
+ icon: getIconForFeed(feedName)
290
+ };
291
+
292
+ // Replace the temporary feed
293
+ const feedIndex = feeds.findIndex(f => f.url === feedUrl);
294
+ if (feedIndex !== -1) {
295
+ feeds[feedIndex] = updatedFeed;
296
+ saveFeeds();
297
+ renderSourcesList();
298
+
299
+ // If this is the only feed, show its articles
300
+ if (feeds.length === 1) {
301
+ const feedTab = document.querySelector(`.source-tab[data-url="${feedUrl}"]`);
302
+ if (feedTab) feedTab.click();
303
+ }
304
+ }
305
+ })
306
+ .catch(error => {
307
+ console.error('Error fetching feed:', error);
308
+ // If we can't fetch the feed, still keep it but with a generic name
309
+ const feedIndex = feeds.findIndex(f => f.url === feedUrl);
310
+ if (feedIndex !== -1) {
311
+ feeds[feedIndex] = {
312
+ url: feedUrl,
313
+ name: new URL(feedUrl).hostname.replace('www.', ''),
314
+ icon: 'rss'
315
+ };
316
+ saveFeeds();
317
+ renderSourcesList();
318
+ }
319
+ });
320
+
321
+ feedUrlInput.value = '';
322
+ }
323
+
324
+ function deleteFeed(url) {
325
+ if (confirm('Are you sure you want to remove this feed?')) {
326
+ feeds = feeds.filter(feed => feed.url !== url);
327
+ saveFeeds();
328
+
329
+ // Also remove articles from this feed
330
+ articles = articles.filter(article => article.feedUrl !== url);
331
+
332
+ // Update UI
333
+ renderSourcesList();
334
+
335
+ if (currentFeed && currentFeed.url === url) {
336
+ // If we deleted the currently selected feed, show all articles
337
+ currentFeed = null;
338
+ const allNewsTab = sourcesList.querySelector('.source-tab:first-child');
339
+ if (allNewsTab) allNewsTab.click();
340
+ }
341
+
342
+ renderArticles(currentFeed ?
343
+ articles.filter(article => article.feedUrl === currentFeed.url) :
344
+ articles);
345
+
346
+ if (feeds.length === 0) {
347
+ showNoFeedsMessage();
348
+ }
349
+ }
350
+ }
351
+
352
+ function fetchAllFeeds() {
353
+ loadingIndicator.classList.remove('hidden');
354
+ newsContainer.classList.add('hidden');
355
+ noFeedsMessage.classList.add('hidden');
356
+
357
+ const fetchPromises = feeds.map(feed => fetchFeed(feed.url));
358
+
359
+ Promise.all(fetchPromises)
360
+ .then(() => {
361
+ updateLastUpdated();
362
+ renderArticles(articles);
363
+ })
364
+ .catch(error => {
365
+ console.error('Error fetching some feeds:', error);
366
+ updateLastUpdated();
367
+ renderArticles(articles);
368
+ });
369
+ }
370
+
371
+ function fetchFeed(url, isNewFeed = false) {
372
+ if (!isNewFeed) {
373
+ loadingIndicator.classList.remove('hidden');
374
+ newsContainer.classList.add('hidden');
375
+ }
376
+
377
+ // Use a CORS proxy to avoid issues with RSS feeds that don't allow cross-origin requests
378
+ const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
379
+
380
+ return fetch(proxyUrl)
381
+ .then(response => {
382
+ if (!response.ok) throw new Error('Network response was not ok');
383
+ return response.json();
384
+ })
385
+ .then(data => {
386
+ // Parse the XML content
387
+ const parser = new DOMParser();
388
+ const xmlDoc = parser.parseFromString(data.contents, "text/xml");
389
+
390
+ // Extract feed title
391
+ let feedTitle = '';
392
+ const titleElement = xmlDoc.querySelector('channel > title');
393
+ if (titleElement) {
394
+ feedTitle = titleElement.textContent;
395
+ }
396
+
397
+ // Extract articles
398
+ const items = xmlDoc.querySelectorAll('item');
399
+ const newArticles = [];
400
+
401
+ items.forEach(item => {
402
+ const title = item.querySelector('title')?.textContent || 'No title';
403
+ const link = item.querySelector('link')?.textContent || '#';
404
+ const description = item.querySelector('description')?.textContent || '';
405
+ const pubDate = item.querySelector('pubDate')?.textContent || '';
406
+ const imageElement = item.querySelector('enclosure[type^="image/"]') ||
407
+ item.querySelector('media\\:content, content') ||
408
+ item.querySelector('image');
409
+ let imageUrl = '';
410
+
411
+ if (imageElement) {
412
+ imageUrl = imageElement.getAttribute('url') ||
413
+ imageElement.getAttribute('href') ||
414
+ '';
415
+ }
416
+
417
+ // Try to extract image from description if not found
418
+ if (!imageUrl && description) {
419
+ const imgRegex = /<img[^>]+src="([^">]+)"/;
420
+ const match = description.match(imgRegex);
421
+ if (match) {
422
+ imageUrl = match[1];
423
+ }
424
+ }
425
+
426
+ newArticles.push({
427
+ title,
428
+ link,
429
+ description: cleanDescription(description),
430
+ pubDate,
431
+ imageUrl,
432
+ feedUrl: url,
433
+ feedName: feedTitle
434
+ });
435
+ });
436
+
437
+ // Remove old articles from this feed
438
+ articles = articles.filter(article => article.feedUrl !== url);
439
+
440
+ // Add new articles
441
+ articles = [...newArticles, ...articles];
442
+
443
+ // Sort by date (newest first)
444
+ articles.sort((a, b) => {
445
+ const dateA = a.pubDate ? new Date(a.pubDate) : new Date(0);
446
+ const dateB = b.pubDate ? new Date(b.pubDate) : new Date(0);
447
+ return dateB - dateA;
448
+ });
449
+
450
+ if (!isNewFeed) {
451
+ renderArticles(currentFeed ?
452
+ articles.filter(article => article.feedUrl === currentFeed.url) :
453
+ articles);
454
+ }
455
+
456
+ return { feed: { title: feedTitle }, articles: newArticles };
457
+ })
458
+ .catch(error => {
459
+ console.error('Error fetching feed:', error);
460
+ if (!isNewFeed) {
461
+ renderArticles(currentFeed ?
462
+ articles.filter(article => article.feedUrl === currentFeed.url) :
463
+ articles);
464
+ }
465
+ throw error;
466
+ });
467
+ }
468
+
469
+ function renderArticles(articlesToShow) {
470
+ loadingIndicator.classList.add('hidden');
471
+
472
+ if (articlesToShow.length === 0) {
473
+ newsContainer.classList.add('hidden');
474
+ if (feeds.length > 0) {
475
+ noFeedsMessage.querySelector('h3').textContent = 'No articles found';
476
+ noFeedsMessage.querySelector('p').textContent = 'Try refreshing or check the feed URL';
477
+ noFeedsMessage.classList.remove('hidden');
478
+ } else {
479
+ showNoFeedsMessage();
480
+ }
481
+ return;
482
+ }
483
+
484
+ newsContainer.innerHTML = '';
485
+ newsContainer.classList.remove('hidden');
486
+ noFeedsMessage.classList.add('hidden');
487
+
488
+ articlesToShow.forEach(article => {
489
+ const articleDate = article.pubDate ? new Date(article.pubDate) : null;
490
+ const timeAgo = articleDate ? getTimeAgo(articleDate) : '';
491
+
492
+ const feed = feeds.find(f => f.url === article.feedUrl) || {};
493
+
494
+ const card = document.createElement('div');
495
+ card.className = 'news-card bg-white rounded-lg overflow-hidden border border-gray-100 hover:shadow-md transition-all fade-in';
496
+ card.innerHTML = `
497
+ <div class="h-48 overflow-hidden">
498
+ ${article.imageUrl ?
499
+ `<img src="${article.imageUrl}" alt="${article.title}" class="w-full h-full object-cover">` :
500
+ `<div class="w-full h-full bg-gray-100 flex items-center justify-center">
501
+ <i class="fas fa-newspaper text-4xl text-gray-300"></i>
502
+ </div>`}
503
+ </div>
504
+ <div class="p-4">
505
+ <div class="flex items-center gap-2 mb-2">
506
+ <span class="text-xs font-medium px-2 py-1 bg-gray-100 rounded-full text-gray-600">
507
+ ${feed.name || article.feedName || 'Unknown Source'}
508
+ </span>
509
+ ${timeAgo ? `<span class="text-xs text-gray-400">${timeAgo}</span>` : ''}
510
+ </div>
511
+ <h3 class="font-semibold text-lg mb-2 line-clamp-2">${article.title}</h3>
512
+ <p class="text-gray-500 text-sm mb-4 line-clamp-2">${article.description}</p>
513
+ <a href="${article.link}" target="_blank" rel="noopener noreferrer"
514
+ class="text-blue-500 hover:text-blue-700 text-sm font-medium flex items-center gap-1">
515
+ Read more <i class="fas fa-external-link-alt text-xs"></i>
516
+ </a>
517
+ </div>
518
+ `;
519
+
520
+ newsContainer.appendChild(card);
521
+ });
522
+ }
523
+
524
+ function showNoFeedsMessage() {
525
+ loadingIndicator.classList.add('hidden');
526
+ newsContainer.classList.add('hidden');
527
+ noFeedsMessage.classList.remove('hidden');
528
+ noFeedsMessage.querySelector('h3').textContent = 'No feeds added yet';
529
+ noFeedsMessage.querySelector('p').textContent = 'Add your first RSS feed to get started';
530
+ }
531
+
532
+ function saveFeeds() {
533
+ localStorage.setItem('rssFeeds', JSON.stringify(feeds));
534
+ }
535
+
536
+ function showError(message) {
537
+ feedError.textContent = message;
538
+ feedError.classList.remove('hidden');
539
+ feedUrlInput.classList.add('border-red-500');
540
+ }
541
+
542
+ function hideError() {
543
+ feedError.classList.add('hidden');
544
+ feedUrlInput.classList.remove('border-red-500');
545
+ }
546
+
547
+ function updateLastUpdated() {
548
+ lastUpdated.textContent = `Updated ${new Date().toLocaleTimeString()}`;
549
+ }
550
+
551
+ // Helper functions
552
+ function isValidUrl(string) {
553
+ try {
554
+ new URL(string);
555
+ return true;
556
+ } catch (_) {
557
+ return false;
558
+ }
559
+ }
560
+
561
+ function cleanDescription(description) {
562
+ // Remove HTML tags
563
+ const stripped = description.replace(/<[^>]*>?/gm, '');
564
+ // Trim and clean up
565
+ return stripped.trim().replace(/\s+/g, ' ');
566
+ }
567
+
568
+ function getTimeAgo(date) {
569
+ const seconds = Math.floor((new Date() - date) / 1000);
570
+
571
+ let interval = Math.floor(seconds / 31536000);
572
+ if (interval >= 1) return `${interval} year${interval === 1 ? '' : 's'} ago`;
573
+
574
+ interval = Math.floor(seconds / 2592000);
575
+ if (interval >= 1) return `${interval} month${interval === 1 ? '' : 's'} ago`;
576
+
577
+ interval = Math.floor(seconds / 86400);
578
+ if (interval >= 1) return `${interval} day${interval === 1 ? '' : 's'} ago`;
579
+
580
+ interval = Math.floor(seconds / 3600);
581
+ if (interval >= 1) return `${interval} hour${interval === 1 ? '' : 's'} ago`;
582
+
583
+ interval = Math.floor(seconds / 60);
584
+ if (interval >= 1) return `${interval} minute${interval === 1 ? '' : 's'} ago`;
585
+
586
+ return 'Just now';
587
+ }
588
+
589
+ function getIconForFeed(feedName) {
590
+ const lowerName = feedName.toLowerCase();
591
+
592
+ if (lowerName.includes('bbc')) return 'globe-europe';
593
+ if (lowerName.includes('new york times') || lowerName.includes('nytimes')) return 'newspaper';
594
+ if (lowerName.includes('npr')) return 'microphone-alt';
595
+ if (lowerName.includes('tech') || lowerName.includes('technology')) return 'laptop-code';
596
+ if (lowerName.includes('sport') || lowerName.includes('espn')) return 'running';
597
+ if (lowerName.includes('business') || lowerName.includes('economy')) return 'chart-line';
598
+ if (lowerName.includes('science')) return 'flask';
599
+ if (lowerName.includes('health')) return 'heartbeat';
600
+
601
+ return 'rss';
602
+ }
603
+ });
604
+ </script>
605
+ </body>
606
+ </html>