v2.0-2
authorxangelo <git@xangelo.ca>
Wed, 6 Apr 2022 17:03:58 +0000 (13:03 -0400)
committerxangelo <git@xangelo.ca>
Wed, 6 Apr 2022 17:03:58 +0000 (13:03 -0400)
This brings to conclusion the initial functionality of the RSS reader
portion of this project. It supports the following functionality:

- Add new RSS feeds by URL
- Tracking read/unread counts
- Marking all unread items as read
- Delete a feed

html/index.html
html/style.css
src/server.ts

index 62d2d88a045264b00ddd785eb0a9945a3f934fa8..47d890f6e183f44134a6ffa9b06ee58b55880db0 100644 (file)
@@ -5,27 +5,23 @@
     <meta charset="utf-8">
     <link rel="stylesheet" href="style.css">
     <script src="https://unpkg.com/htmx.org@1.7.0"></script>
+    <base target="_blank">
   </head>
   <body class="app">
     <div id="feed-pane" valign="top" hx-get="/feeds" hx-trigger="load, newFeed from:body, every 900s" hx-target="#feed-list">
       <h1>Feed List</h1>
+      <div id="feed-list"></div>
       <div id="add-feed">
         <form hx-post="/feeds">
           <input type="url" name="link" placeholder="Link to RSS Feed">
-          <button type="submit">Add</button>
+          <button type="submit" class="btn">Add</button>
         </form>
       </div>
-      <div id="feed-list"></div>
-    </div>
-    <div id="list-pane">
-      <div>
-        <table><thead><tr><th style="width: 80%">Title</th><th>Publish Date</th></head></table>
-      </div>
-      <ul class="scrollable list" id="feed-item-list"></ul>
     </div>
+    <div id="list-pane"></div>
     <div id="reading-pane"></div>
     <div id="status-bar">
-      A project by <a href="https://xangelo.ca">xangelo</a> that is still in active <a href="https://git.xangelo.ca">development</a>
+      A project by <a href="https://xangelo.ca">xangelo</a> that is still in active <a href="https://git.xangelo.ca">development</a> (2022)
     </div>
   </body>
   <script>
index 53827b1f772ac104250f8ed4f9366b9ddcfce54f..73fb32c8c3a89a11fed045e5ad90deaef6cc5f01 100644 (file)
@@ -30,7 +30,7 @@ h1 {
 }
 
 #feed-item-list {
-  height: calc(100% - 30px);
+  height: calc(100% - 70px);
 }
 #feed-item-list span {
   float: right;
@@ -50,13 +50,13 @@ h1 {
 #reading-pane img {
   max-width: 100%;
 }
-#feed-content, #meta {
+#feed-content, #meta, #feed-info {
   padding: 10px;
 }
 #feed-content {
   height: calc(100% - 100px)
 }
-#meta {
+#meta, #feed-info {
   background-color: #ddd;
   font-size: 0.9rem;
   max-height: 100px;
@@ -73,6 +73,18 @@ h1 {
   text-align: right;
 }
 
+.btn {
+  display: inline-block;
+  border: solid 1px #888;
+  border-radius: 4px;
+  padding: 4px 10px;
+  background-color: #eee;
+  text-decoration: none;
+}
+.btn:hover {
+  background-color: #ddd;
+}
+
 
 .scrollable {
   overflow: auto;
@@ -96,7 +108,7 @@ h1 {
   background-color: #eee;
 }
 #feed-pane a.active {
-  background-color: #eee;
+  background-color: #fff2cc;
 }
 #feed-pane a.unread {
   font-weight: bold;
@@ -105,6 +117,13 @@ h1 {
   content: '\21D2';
   float: right;
 }
+#feed-list {
+  height: calc(100% - 70px);
+}
+#add-feed {
+  padding: 5px;
+  border-top: solid 1px #000;
+}
 
 #list-pane table {
   width: 100%;
@@ -120,19 +139,19 @@ h1 {
 #list-pane th {
   padding: 4px;
 }
-#list-pane table th:nth-child(2) {
+#feed-actions {
+  float: right;
 }
-#list-pane a {
+#feed-item-list a {
   display: block;
   padding: 4px;
 }
-#feed-pane a.active {
+#feed-item-list a.active {
   background-color: #fff5e3;
 }
-#list-pane a.active {
+#feed-item-list a.active {
   background-color: #e3f2ff;
 }
-
-#list-pane a.unread {
+#feed-item-list a.unread {
   font-weight: bold;
 }
index f14064906fb45858d6421aaec4803e769d5780a3..c2a3fdaf1163b43d80da4e7dd6ce0f1525fac57a 100644 (file)
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
 import { ingest, ingestSingle } from './ingester';
 import {RSSParser} from './parsers/rss';
 import bodyParser from 'body-parser';
+import {BaseParser} from './parsers/base';
 
 
 const app = express();
@@ -63,8 +64,12 @@ apiPost('/feeds', async (req, res): Promise<any> => {
   // get info about the feed
   const url = req.body.link;
 
-  const rss = new RSSParser();
-  const feedData = await rss.parse(url);
+  let parser: BaseParser;
+
+  // based on the url, we should figure out if this is a reddit or rss feed
+  parser = new RSSParser();
+
+  const feedData = await parser.parse(url);
 
   const title = feedData.title;
   const id = uuidv4();
@@ -108,7 +113,7 @@ apiGet('/feeds', async (req, res): Promise<any> => {
     const display = feed.unread ? `(${feed.unread})` : '';
     const first = idx === 0;
 
-    return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#feed-item-list" data-feed-id="${feed.id}">${feed.title} 
+    return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title} 
     <span class="unread-count">${display}</span>
     </a></li>`
   }).join("\n")}</ul>`;
@@ -120,13 +125,36 @@ function reasonable(date: Date): string {
   return `${date.getFullYear()}-${month}-${day}`;
 }
 
-apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
+apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
   const id = req.params.feed_id;
+  return query.getFeedInfo.get(id);
+}, (feed: any): string => {
+  return `
+  <b>Feed:</b> ${feed.title}<br>
+  <b>Link:</b> ${feed.link}<br>
+  `;
+});
 
-  return query.getFeedsFor.all(id, 0);
+apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
+  const id = req.params.feed_id;
+  return {
+    items: query.getFeedsFor.all(id, 0),
+    info: query.getFeedInfo.get(id)
+  }
 }, (feedData: any): string => {
   return `
-  ${feedData.map((item: any, index: number) => {
+      <div id="feed-info">
+      <div id="feed-actions">
+        <a href="#" class="btn" hx-post="/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
+        <a href="#" class="btn" hx-delete="/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
+      </div>
+      <b>Feed: </b>${feedData.info.title}<br>
+      </div>
+      <div>
+        <table><thead><tr><th style="width: 80%">Title</th><th>Publish Date</th></head></table>
+      </div>
+      <ul class="scrollable list" id="feed-item-list">
+  ${feedData.items.map((item: any, index: number) => {
     const read = !!item.read_at;
     return `<li>
       <a href="#" class="${index === 0 ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/feeds/${item.feed_id}/items/${item.id}" hx-trigger="${index === 0 ? 'load,': ''}click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
@@ -134,9 +162,16 @@ apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
       </a>
     </li>
   `}).join("\n")}
+  </ul>
   `;
 });
 
+apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise<any> => {
+  const id = req.params.feed_id;
+  query.readAllItems.run(id);
+  return;
+});
+
 apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
   const feed_id = req.params.feed_id;
   const item_id = req.params.item_id;
@@ -157,6 +192,14 @@ apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
   return output.content;
 });
 
+apiDelete('/feeds/:feed_id', async (req, res) => {
+  const id = req.params.feed_id;
+
+  query.deleteFeed.run(id);
+  res.setHeader('HX-Trigger', 'newFeed');
+  return;
+});
+
 async function periodicIngest() {
   await ingest();
   setTimeout(periodicIngest, 1000 * 60 * 10);