Update the UI (#10)
* Move the emoji * Convert to a grid layout * Ellipsize the feed urls * Add link to source on GitHub * Always be updating the relative time of the feed * Make "recent" within the last 8 hours * Sort the feeds alphabetically
This commit is contained in:
parent
567b29995f
commit
5f27f8a16e
7 changed files with 217 additions and 89 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -5,34 +5,132 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>📰 Carter's RSS Feeds</title>
|
<title>Carter's RSS Feeds</title>
|
||||||
<link rel="icon" href="news-emoji.svg" sizes="any" type="image/svg+xml">
|
<link rel="icon" href="news-emoji.svg" sizes="any" type="image/svg+xml">
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
<script type="module" defer>
|
||||||
|
/**
|
||||||
|
* from @feelinglovelynow/get-relative-time
|
||||||
|
* MIT License
|
||||||
|
* Copyright (c) 2023 Feeling Lovely Now
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Simple function that recieves a `Date` object that is in the future or is in the past and gives back the relative time using `Intl.RelativeTimeFormat('en', { numeric: 'auto' })`
|
||||||
|
* Examples: `[ "last year", "6 months ago", "4 weeks ago", "7 days ago", "now", "in 1 minute", "in 6 hours", "tomorrow", "in 3 days", "in 4 weeks", "next month", "in 2 months", "in 12 months", "next year" ]`
|
||||||
|
* @param { Date } date
|
||||||
|
* @throws { { id: 'fln__get-relative-time__invalid-date', message: 'Please pass getRelativeTime() a valid date object', _errorData: { date } } } - `IF (!(date instanceof Date) || date.toString() === 'Invalid Date')`
|
||||||
|
* @returns { string }
|
||||||
|
*/
|
||||||
|
function getRelativeTime(date) {
|
||||||
|
if (!(date instanceof Date) || date.toString() === 'Invalid Date') throw { id: 'fln__get-relative-time__invalid-date', message: 'Please pass getRelativeTime() a valid date object', _errorData: { date } }
|
||||||
|
else {
|
||||||
|
/** @type { string } */
|
||||||
|
let response
|
||||||
|
|
||||||
|
const timestamp = date.getTime()
|
||||||
|
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||||
|
|
||||||
|
const diffMilliSeconds = timestamp - ((new Date()).getTime())
|
||||||
|
const diffSeconds = Math.round(diffMilliSeconds / 1000)
|
||||||
|
const diffMinutes = Math.round(diffSeconds / 60)
|
||||||
|
const diffHours = Math.round(diffMinutes / 60)
|
||||||
|
const diffDays = Math.round(diffHours / 24)
|
||||||
|
|
||||||
|
if (!Boolean(diffDays) && !Boolean(diffHours) && !Boolean(diffMinutes)) response = rtf.format(diffSeconds, 'second')
|
||||||
|
else if (!Boolean(diffDays) && !Boolean(diffHours)) response = rtf.format(diffMinutes, 'minute')
|
||||||
|
else if (!Boolean(diffDays)) response = rtf.format(diffHours, 'hour')
|
||||||
|
else if (diffDays < -365) response = rtf.format(Math.round(diffDays / 365), 'year')
|
||||||
|
else if (diffDays < -33) response = rtf.format(Math.round(diffDays / 30), 'month')
|
||||||
|
else if (diffDays < -7) response = rtf.format(Math.round(diffDays / 7), 'week')
|
||||||
|
else if (diffDays > 365) response = rtf.format(Math.round(diffDays / 365), 'year')
|
||||||
|
else if (diffDays > 33) response = rtf.format(Math.round(diffDays / 30), 'month')
|
||||||
|
else if (diffDays > 7) response = rtf.format(Math.round(diffDays / 7), 'week')
|
||||||
|
else response = rtf.format(diffDays, 'day')
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param { Date } date
|
||||||
|
* @returns { number }
|
||||||
|
*/
|
||||||
|
function getUpdateInterval(date) {
|
||||||
|
const now = new Date();
|
||||||
|
const diffMilliSeconds = date.getTime() - now.getTime();
|
||||||
|
const diffSeconds = Math.round(diffMilliSeconds / 1000);
|
||||||
|
if (diffSeconds < 60) return 1000;
|
||||||
|
if (diffSeconds < 3600) return 1000 * 60;
|
||||||
|
if (diffSeconds < 86400) return 1000 * 60 * 60;
|
||||||
|
return 1000 * 60 * 60 * 24;
|
||||||
|
}
|
||||||
|
class RelativeTime extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const date = new Date(parseInt(this.getAttribute("data-time")));
|
||||||
|
this.textContent = getRelativeTime(date);
|
||||||
|
setInterval(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.textContent = getRelativeTime(date);
|
||||||
|
});
|
||||||
|
}, getUpdateInterval(date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define("relative-time", RelativeTime);
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Carter's RSS Feeds</h1>
|
<header>
|
||||||
|
<h1>📰 Carter's RSS Feeds</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
{% for group, feeds in data %}
|
{% for group, feeds in data %}
|
||||||
|
<section>
|
||||||
<h2>{{ group }}</h2>
|
<h2>{{ group }}</h2>
|
||||||
{% for feed in feeds %}
|
{% for feed in feeds %}
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
<span class="feed-title">{{ feed.title }}</span>
|
<span class="feed-title">{{ feed.title }}</span>
|
||||||
<span class="feed-url">({{ feed.feed }})</span>
|
<span class="feed-url" title="{{feed.feed}}">({{ feed.feed }})</span>
|
||||||
</summary>
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in feed.items %}
|
{% for item in feed.items %}
|
||||||
<li class="{% if item.isRecent %}has-recent{% endif %}">
|
<li class="{% if item.isRecent %}has-recent{% endif %}">
|
||||||
<div><a class="article-title" href="{{ item.link }}" target="_blank" rel="noopener norefferer nofollow">{{
|
<div>
|
||||||
item.title }}</a>
|
<a class="article-title" href="{{ item.link }}" target="_blank" rel="noopener norefferer nofollow">{{
|
||||||
<div class="article-timestamp">{{ item.timestamp | relative }}</div>
|
item.title | safe}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="article-timestamp">
|
||||||
|
<relative-time data-time="{{item.timestamp}}">{{ item.timestamp | relative}}</relative-time>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
{% endfor %} {% endfor %} {% if errors | length > 0 %}
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
{% if errors | length > 0 %}
|
||||||
<h2>Errors</h2>
|
<h2>Errors</h2>
|
||||||
<p>There were errors trying to parse these feeds:</p>
|
<p>There were errors trying to parse these feeds:</p>
|
||||||
<ul class="errors">
|
<ul class="errors">
|
||||||
|
|
@ -41,15 +139,18 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<footer>
|
||||||
<br />
|
<hr>
|
||||||
<hr />
|
|
||||||
<p>Last updated {{ now }}.</p>
|
<p>Last updated {{ now }}.</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/carterworks/rss-reader">View on GitHub</a>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Powered by
|
Powered by
|
||||||
<a href="https://github.com/georgemandis/bubo-rss">Bubo Reader (v{{ info.version }})</a>, a project by <a
|
<a href="https://github.com/georgemandis/bubo-rss">Bubo Reader (v{{ info.version }})</a>, a project by <a
|
||||||
href="https://george.mand.is">George Mandis</a>. ❤️
|
href="https://george.mand.is">George Mandis</a>. ❤️
|
||||||
</p>
|
</p>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@feelinglovelynow/get-relative-time": "^1.1.2",
|
||||||
"chalk": "^5.2.0",
|
"chalk": "^5.2.0",
|
||||||
"javascript-time-ago": "^2.5.10",
|
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"rss-parser": "^3.13.0"
|
"rss-parser": "^3.13.0"
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@
|
||||||
"theme_color": "#FCF5E4",
|
"theme_color": "#FCF5E4",
|
||||||
"description": "Updates from RSS feeds that Carter likes",
|
"description": "Updates from RSS feeds that Carter likes",
|
||||||
"id": "/",
|
"id": "/",
|
||||||
"icons": [{
|
"icons": [
|
||||||
|
{
|
||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"src": "news-emoji.svg",
|
"src": "news-emoji.svg",
|
||||||
"type": "image/svg+xml"
|
"type": "image/svg+xml"
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,17 @@ body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
gap: 1em;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
section ul {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-icon {
|
.inline-icon {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
|
@ -19,6 +30,7 @@ body {
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary,
|
summary,
|
||||||
|
|
@ -42,13 +54,18 @@ details:has(li.has-recent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
details,
|
|
||||||
.errors li {
|
.errors li {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed-url {
|
.feed-url {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-timestamp,
|
.article-timestamp,
|
||||||
|
|
|
||||||
19
src/index.ts
19
src/index.ts
|
|
@ -72,9 +72,20 @@ const finishBuild: () => void = async () => {
|
||||||
|
|
||||||
process.stdout.write("\nDone fetching everything!\n");
|
process.stdout.write("\nDone fetching everything!\n");
|
||||||
|
|
||||||
|
// sort all the categories and the feeds alphabetically
|
||||||
|
const sortedFeeds: Feeds = {};
|
||||||
|
const sortedKeys = Object.keys(contentFromAllFeeds).sort((a, b) =>
|
||||||
|
a.localeCompare(b),
|
||||||
|
);
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
sortedFeeds[key] = contentFromAllFeeds[key].sort((a, b) =>
|
||||||
|
a.title.localeCompare(b.title),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// generate the static HTML output from our template renderer
|
// generate the static HTML output from our template renderer
|
||||||
const output = render({
|
const output = render({
|
||||||
data: contentFromAllFeeds,
|
data: sortedFeeds,
|
||||||
errors: errors,
|
errors: errors,
|
||||||
info: buboInfo,
|
info: buboInfo,
|
||||||
});
|
});
|
||||||
|
|
@ -125,9 +136,9 @@ const processFeed =
|
||||||
item.title = getTitle(item);
|
item.title = getTitle(item);
|
||||||
item.link = getLink(item);
|
item.link = getLink(item);
|
||||||
const timestamp = new Date(Number.parseInt(item.timestamp));
|
const timestamp = new Date(Number.parseInt(item.timestamp));
|
||||||
const yesterday = new Date();
|
const eightHoursAgo = new Date();
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
eightHoursAgo.setHours(eightHoursAgo.getHours() - 8);
|
||||||
item.isRecent = timestamp > yesterday;
|
item.isRecent = timestamp > eightHoursAgo;
|
||||||
}
|
}
|
||||||
|
|
||||||
contents.hasRecent = contents.items.some((item) => item.isRecent);
|
contents.hasRecent = contents.items.some((item) => item.isRecent);
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,15 @@
|
||||||
import nunjucks from "nunjucks";
|
import nunjucks from "nunjucks";
|
||||||
const env: nunjucks.Environment = nunjucks.configure({ autoescape: true });
|
const env: nunjucks.Environment = nunjucks.configure({ autoescape: true });
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { getRelativeTime } from "@feelinglovelynow/get-relative-time";
|
||||||
import type { Feeds, JSONValue } from "./@types/bubo";
|
import type { Feeds, JSONValue } from "./@types/bubo";
|
||||||
import TimeAgo from "javascript-time-ago";
|
|
||||||
import en from "javascript-time-ago/locale/en";
|
|
||||||
|
|
||||||
TimeAgo.addDefaultLocale(en);
|
|
||||||
const timeFormatter = new TimeAgo("en-US");
|
|
||||||
/**
|
/**
|
||||||
* Global filters for my Nunjucks templates
|
* Global filters for my Nunjucks templates
|
||||||
*/
|
*/
|
||||||
env.addFilter("relative", (dateString): string => {
|
env.addFilter("relative", (dateString): string => {
|
||||||
const date: Date = new Date(Number.parseInt(dateString));
|
const date: Date = new Date(Number.parseInt(dateString));
|
||||||
return !Number.isNaN(date.getTime()) ? timeFormatter.format(date) : dateString;
|
return !Number.isNaN(date.getTime()) ? getRelativeTime(date) : dateString;
|
||||||
});
|
});
|
||||||
|
|
||||||
env.addFilter("formatTime", (dateString): string => {
|
env.addFilter("formatTime", (dateString): string => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue