summary refs log tree commit diff
path: root/assets/js/rss-feed-preview.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js/rss-feed-preview.js')
-rw-r--r--assets/js/rss-feed-preview.js236
1 files changed, 236 insertions, 0 deletions
diff --git a/assets/js/rss-feed-preview.js b/assets/js/rss-feed-preview.js
new file mode 100644
index 0000000..2929622
--- /dev/null
+++ b/assets/js/rss-feed-preview.js
@@ -0,0 +1,236 @@
+/**
+ * RSS/Atom Feed Preview for Links Table
+ */
+
+(function() {
+    const existingPreviews = document.querySelectorAll('#rss-feed-preview');
+    existingPreviews.forEach(el => el.remove());
+  
+    const CORS_PROXY = 'https://cors-anywhere.mayx.eu.org/?';
+  
+    const createPreviewElement = () => {
+      const existingPreview = document.getElementById('rss-feed-preview');
+      if (existingPreview) {
+        return existingPreview;
+      }
+  
+      const previewEl = document.createElement('div');
+      previewEl.id = 'rss-feed-preview';
+      previewEl.style.cssText = `
+        position: fixed;
+        display: none;
+        width: 300px;
+        max-height: 400px;
+        overflow-y: auto;
+        background-color: white;
+        border: 1px solid #ccc;
+        border-radius: 5px;
+        padding: 10px;
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+        z-index: 1000;
+        font-size: 14px;
+        line-height: 1.4;
+      `;
+      document.body.appendChild(previewEl);
+      return previewEl;
+    };
+  
+    const parseRSS = (xmlText) => {
+      const parser = new DOMParser();
+      const xml = parser.parseFromString(xmlText, 'text/xml');
+  
+      const rssItems = xml.querySelectorAll('item');
+      if (rssItems.length > 0) {
+        return Array.from(rssItems).slice(0, 5).map(item => {
+          return {
+            title: item.querySelector('title')?.textContent || 'No title',
+            date: item.querySelector('pubDate')?.textContent || 'No date',
+          };
+        });
+      }
+  
+      const atomItems = xml.querySelectorAll('entry');
+      if (atomItems.length > 0) {
+        return Array.from(atomItems).slice(0, 5).map(item => {
+          return {
+            title: item.querySelector('title')?.textContent || 'No title',
+            date: item.querySelector('updated')?.textContent || 'No date',
+          };
+        });
+      }
+  
+      return null;
+    };
+  
+    const checkFeed = async (url) => {
+      try {
+        const response = await fetch(CORS_PROXY + url);
+        if (!response.ok) {
+          return null;
+        }
+  
+        const text = await response.text();
+        return parseRSS(text);
+      } catch (error) {
+        return null;
+      }
+    };
+  
+    const findFeedUrl = async (siteUrl, linkElement) => {
+      if (linkElement && linkElement.hasAttribute('data-feed')) {
+        const dataFeedUrl = linkElement.getAttribute('data-feed');
+        if (dataFeedUrl) {
+          const feedItems = await checkFeed(dataFeedUrl);
+          if (feedItems) {
+            return { url: dataFeedUrl, items: feedItems };
+          }
+        }
+      }
+  
+      return null;
+    };
+  
+    const escapeHTML = (str) => {
+      return String(str).replace(/[&<>"'/]/g, (c) => ({
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+        '"': '&quot;',
+        "'": '&#39;',
+        '/': '&#x2F;'
+      }[c]));
+    };
+
+    const renderFeedItems = (previewEl, items, siteName) => {
+      if (!items || items.length === 0) {
+        previewEl.innerHTML = '<p>No feed items found.</p>';
+        return;
+      }
+  
+      let html = `<h3>Latest from ${siteName}</h3><ul style="list-style: none; padding: 0; margin: 0;">`;
+  
+      items.forEach(item => {
+        const safeTitle = escapeHTML(item.title);
+        const safeDate = escapeHTML(new Date(item.date).toLocaleDateString());
+        html += `
+          <li style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
+            <div style="color: #24292e; font-weight: bold;">
+              ${safeTitle}
+            </div>
+            <div style="color: #586069; font-size: 12px; margin: 3px 0;">
+              ${safeDate}
+            </div>
+          </li>
+        `;
+      });
+  
+      html += '</ul>';
+      previewEl.innerHTML = html;
+    };
+  
+    const positionPreview = (previewEl, event) => {
+      const viewportWidth = window.innerWidth;
+      const viewportHeight = window.innerHeight;
+  
+      let left = event.clientX + 20;
+      let top = event.clientY + 20;
+  
+      const rect = previewEl.getBoundingClientRect();
+  
+      if (left + rect.width > viewportWidth) {
+        left = event.clientX - rect.width - 20;
+      }
+  
+      if (top + rect.height > viewportHeight) {
+        top = event.clientY - rect.height - 20;
+      }
+  
+      left = Math.max(10, left);
+      top = Math.max(10, top);
+  
+      previewEl.style.left = `${left}px`;
+      previewEl.style.top = `${top}px`;
+    };
+  
+    const initFeedPreview = () => {
+      const previewEl = createPreviewElement();
+  
+      const tableLinks = document.querySelectorAll('main table tbody tr td a');
+  
+      const feedCache = {};
+  
+      let currentLink = null;
+      let loadingTimeout = null;
+  
+      tableLinks.forEach(link => {
+        link.addEventListener('mouseenter', async (event) => {
+          currentLink = link;
+          const url = link.getAttribute('href');
+          const siteName = link.textContent;
+  
+          previewEl.innerHTML = '<p>Checking for RSS/Atom feed...</p>';
+          previewEl.style.display = 'block';
+          positionPreview(previewEl, event);
+  
+          if (loadingTimeout) {
+            clearTimeout(loadingTimeout);
+          }
+  
+          loadingTimeout = setTimeout(async () => {
+            if (feedCache[url]) {
+              renderFeedItems(previewEl, feedCache[url].items, siteName);
+              positionPreview(previewEl, event); // Reposition after content is loaded
+              return;
+            }
+  
+            const feedData = await findFeedUrl(url, link);
+  
+            if (currentLink === link) {
+              if (feedData) {
+                feedCache[url] = feedData;
+                renderFeedItems(previewEl, feedData.items, siteName);
+                positionPreview(previewEl, event); // Reposition after content is loaded
+              } else {
+                previewEl.style.display = 'none';
+              }
+            }
+          }, 300);
+        });
+  
+        link.addEventListener('mousemove', (event) => {
+          if (previewEl.style.display === 'block') {
+            window.requestAnimationFrame(() => {
+              positionPreview(previewEl, event);
+            });
+          }
+        });
+  
+        link.addEventListener('mouseleave', () => {
+          if (loadingTimeout) {
+            clearTimeout(loadingTimeout);
+            loadingTimeout = null;
+          }
+  
+          currentLink = null;
+          previewEl.style.display = 'none';
+        });
+      });
+  
+      document.addEventListener('click', (event) => {
+        if (!previewEl.contains(event.target)) {
+          previewEl.style.display = 'none';
+        }
+      });
+    };
+  
+    if (!window.rssFeedPreviewInitialized) {
+      window.rssFeedPreviewInitialized = true;
+  
+      if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', initFeedPreview);
+      } else {
+        initFeedPreview();
+      }
+    }
+  })();
+  
\ No newline at end of file