<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>
}
#feed-item-list {
- height: calc(100% - 30px);
+ height: calc(100% - 70px);
}
#feed-item-list span {
float: right;
#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;
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;
background-color: #eee;
}
#feed-pane a.active {
- background-color: #eee;
+ background-color: #fff2cc;
}
#feed-pane a.unread {
font-weight: bold;
content: '\21D2';
float: right;
}
+#feed-list {
+ height: calc(100% - 70px);
+}
+#add-feed {
+ padding: 5px;
+ border-top: solid 1px #000;
+}
#list-pane table {
width: 100%;
#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;
}
import { ingest, ingestSingle } from './ingester';
import {RSSParser} from './parsers/rss';
import bodyParser from 'body-parser';
+import {BaseParser} from './parsers/base';
const app = express();
// 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();
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>`;
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}
</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;
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);