Skip to content

Commit 6df95c9

Browse files
committed
✨ Made an RSS feed widget
1 parent f61366c commit 6df95c9

File tree

4 files changed

+307
-1
lines changed

4 files changed

+307
-1
lines changed

docs/widgets.md

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Widgets
22

3-
Dashy has support for displaying dynamic content in the form of widgets. There are several built-in widgets availible out-of-the-box (with more on the way!) as well as support for custom widgets to display stats from almost any service with an accessible API.
3+
Dashy has support for displaying dynamic content in the form of widgets. There are several built-in widgets available out-of-the-box as well as support for custom widgets to display stats from almost any service with an API.
44

55
##### Contents
66
- [General Widgets](#general-widgets)
@@ -9,6 +9,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
99
- [Weather Forecast](#weather-forecast)
1010
- [Crypto Watch List](#crypto-watch-list)
1111
- [Crypto Price History](#crypto-token-price-history)
12+
- [RSS Feed](#rss-feed)
1213
- [XKCD Comics](#xkcd-comics)
1314
- [TFL Status](#tfl-status)
1415
- [Exchange Rates](#exchange-rates)
@@ -47,6 +48,8 @@ A simple, live-updating time and date widget with time-zone support. All options
4748
hideDate: false
4849
```
4950
51+
---
52+
5053
### Weather
5154
5255
A simple, live-updating local weather component, showing temperature, conditions and more info.
@@ -73,6 +76,8 @@ A simple, live-updating local weather component, showing temperature, conditions
7376
hideDetails: false
7477
```
7578

79+
---
80+
7681
### Weather Forecast
7782

7883
Displays the weather (temperature and conditions) for the next few days for a given location. Note that this requires either the free [OpenWeatherMap Student Plan](https://home.openweathermap.org/students), or the Premium Plan.
@@ -100,6 +105,9 @@ Displays the weather (temperature and conditions) for the next few days for a gi
100105
units: imperial
101106
```
102107

108+
---
109+
110+
103111
### Crypto Watch List
104112

105113
Keep track of price changes of your favorite crypto assets. Data is fetched from [CoinGecko](https://www.coingecko.com/)
@@ -139,6 +147,8 @@ Or
139147
- dogecoin
140148
```
141149

150+
---
151+
142152
### Crypto Token Price History
143153

144154
Shows recent price history for a given crypto asset, using price data fetched from [CoinGecko](https://www.coingecko.com/)
@@ -163,6 +173,35 @@ Shows recent price history for a given crypto asset, using price data fetched fr
163173
numDays: 7
164174
```
165175

176+
---
177+
178+
### RSS Feed
179+
180+
Display news and updates from any RSS-enabled service.
181+
182+
<p align="center"><img width="600" src="https://i.ibb.co/N9mvLh4/rss-feed.png" /></p>
183+
184+
##### Options
185+
186+
**Field** | **Type** | **Required** | **Description**
187+
--- | --- | --- | ---
188+
**`rssUrl`** | `string` | Required | The URL location of your RSS feed
189+
**`apiKey`** | `string` | _Optional_ | An API key for [rss2json](https://rss2json.com/). It's free, and will allow you to make 10,000 requests per day, you can sign up [here](https://rss2json.com/sign-up)
190+
**`limit`** | `number` | _Optional_ | The number of posts to return. If you haven't specified an API key, this will be limited to 10
191+
**`orderBy`** | `string` | _Optional_ | How results should be sorted. Can be either `pubDate`, `author` or `title`. Defaults to `pubDate`
192+
**`orderDirection`** | `string` | _Optional_ | Order direction of feed items to return. Can be either `asc` or `desc`. Defaults to `desc`
193+
194+
##### Example
195+
196+
```yaml
197+
- type: rss-feed
198+
options:
199+
rssUrl: https://www.schneier.com/blog/atom.xml
200+
apiKey: xxxx
201+
```
202+
203+
---
204+
166205
### XKCD Comics
167206

168207
Have a laugh with the daily comic from [XKCD](https://xkcd.com/). A classic webcomic website covering everything from Linux, math, romance, science and language.
@@ -183,6 +222,8 @@ Have a laugh with the daily comic from [XKCD](https://xkcd.com/). A classic webc
183222
comic: latest
184223
```
185224

225+
---
226+
186227
### TFL Status
187228

188229
Shows real-time tube status of the London Underground. All options are optional.
@@ -214,6 +255,8 @@ Shows real-time tube status of the London Underground. All options are optional.
214255
- Central
215256
```
216257

258+
---
259+
217260
### Exchange Rates
218261

219262
Display current FX rates in your native currency
@@ -242,6 +285,8 @@ Display current FX rates in your native currency
242285
- KPW
243286
```
244287

288+
---
289+
245290
### Stock Price History
246291

247292
Shows recent price history for a given publicly-traded stock or share
@@ -265,6 +310,8 @@ Shows recent price history for a given publicly-traded stock or share
265310
apiKey: PGUWSWD6CZTXMT8N
266311
```
267312

313+
---
314+
268315
### Joke
269316

270317
Renders a programming or generic joke. Data is fetched from the [JokesAPI](https://github.com/Sv443/JokeAPI) by @Sv443
@@ -289,6 +336,8 @@ Renders a programming or generic joke. Data is fetched from the [JokesAPI](https
289336
category: Programming
290337
```
291338

339+
---
340+
292341
### Flight Data
293342

294343
Displays airport departure and arrival flights, using data from [AeroDataBox](https://www.aerodatabox.com/). Useful if you live near an airport and often wonder where the flight overhead is going to. Hover over a row for more flight data.
@@ -331,6 +380,16 @@ Embed any webpage into your dashboard as a widget.
331380
--- | --- | --- | ---
332381
**`url`** | `string` | Required | The URL to the webpage to embed
333382

383+
##### Example
384+
385+
```yaml
386+
- type: iframe
387+
options:
388+
url: https://fiatleak.com/
389+
```
390+
391+
---
392+
334393
### HTML Embedded Widget
335394

336395
Many websites and apps provide their own embeddable widgets. These can be used with Dashy using the Embed widget, which lets you dynamically embed and HTML, CSS or JavaScript contents.

src/components/Widgets/RssFeed.vue

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<template>
2+
<div class="rss-wrapper">
3+
<!-- Feed Meta Info -->
4+
<a class="meta-container" v-if="meta" :href="meta.link" :title="meta.description">
5+
<img class="feed-icon" :src="meta.image" v-if="meta.image" />
6+
<div class="feed-text">
7+
<p class="feed-title">{{ meta.title }}</p>
8+
<p class="feed-author" v-if="meta.author">By {{ meta.author }}</p>
9+
</div>
10+
</a>
11+
<!-- Feed Content -->
12+
<div class="post-wrapper" v-if="posts">
13+
<div class="post-row" v-for="(post, indx) in posts" :key="indx">
14+
<a class="post-top" :href="post.link">
15+
<img class="post-img" :src="post.image" v-if="post.image">
16+
<div class="post-title-wrap">
17+
<p class="post-title">{{ post.title }}</p>
18+
<p class="post-date">
19+
{{ post.date | formatDate }} {{ post.author | formatAuthor }}
20+
</p>
21+
</div>
22+
</a>
23+
<div class="post-body" v-html="post.description"></div>
24+
<a class="continue-reading-btn" :href="post.link">Continue Reading</a>
25+
</div>
26+
</div>
27+
<!-- End Feed Content -->
28+
</div>
29+
</template>
30+
31+
<script>
32+
import axios from 'axios';
33+
import WidgetMixin from '@/mixins/WidgetMixin';
34+
import { widgetApiEndpoints } from '@/utils/defaults';
35+
36+
export default {
37+
mixins: [WidgetMixin],
38+
components: {},
39+
data() {
40+
return {
41+
meta: null,
42+
posts: null,
43+
};
44+
},
45+
mounted() {
46+
this.fetchData();
47+
},
48+
computed: {
49+
/* The URL to users atom-format RSS feed */
50+
rssUrl() {
51+
if (!this.options.rssUrl) this.error('Missing feed URL');
52+
return encodeURIComponent(this.options.rssUrl || '');
53+
},
54+
apiKey() {
55+
return this.options.apiKey;
56+
},
57+
limit() {
58+
const usersChoice = this.options.limit;
59+
if (usersChoice && !Number.isNaN(usersChoice)) return usersChoice;
60+
return 10;
61+
},
62+
orderBy() {
63+
const usersChoice = this.options.orderBy;
64+
const options = ['title', 'pubDate', 'author'];
65+
if (usersChoice && options.includes(usersChoice)) return usersChoice;
66+
return 'pubDate';
67+
},
68+
orderDirection() {
69+
const usersChoice = this.options.orderBy;
70+
if (usersChoice && (usersChoice === 'desc' || usersChoice === 'asc')) return usersChoice;
71+
return 'desc';
72+
},
73+
endpoint() {
74+
const apiKey = this.apiKey ? `&api_key=${this.apiKey}` : '';
75+
const limit = this.limit && this.apiKey ? `&count=${this.limit}` : '';
76+
const orderBy = this.orderBy && this.apiKey ? `&order_by=${this.orderBy}` : '';
77+
const direction = this.orderDirection ? `&order_dir=${this.orderDirection}` : '';
78+
return `${widgetApiEndpoints.rssToJson}?rss_url=${this.rssUrl}`
79+
+ `${apiKey}${limit}${orderBy}${direction}`;
80+
},
81+
},
82+
filters: {
83+
formatDate(timestamp) {
84+
const localFormat = navigator.language;
85+
const dateFormat = { weekday: 'short', day: 'numeric', month: 'short' };
86+
const date = new Date(timestamp).toLocaleDateString(localFormat, dateFormat);
87+
return date;
88+
},
89+
formatAuthor(author) {
90+
return author ? `by ${author}` : '';
91+
},
92+
},
93+
methods: {
94+
/* Extends mixin, and updates data. Called by parent component */
95+
update() {
96+
this.startLoading();
97+
this.fetchData();
98+
},
99+
/* Make GET request to Rss2Json */
100+
fetchData() {
101+
axios.get(this.endpoint)
102+
.then((response) => {
103+
this.processData(response.data);
104+
})
105+
.catch((error) => {
106+
this.error('Unable to RSS feed', error);
107+
})
108+
.finally(() => {
109+
this.finishLoading();
110+
});
111+
},
112+
/* Assign data variables to the returned data */
113+
processData(data) {
114+
const { feed, items } = data;
115+
this.meta = {
116+
title: feed.title,
117+
link: feed.link,
118+
author: feed.author,
119+
description: feed.description,
120+
image: feed.image,
121+
};
122+
const posts = [];
123+
items.forEach((post) => {
124+
posts.push({
125+
title: post.title,
126+
description: post.description,
127+
image: post.thumbnail,
128+
author: post.author,
129+
date: post.pubDate,
130+
link: post.link,
131+
});
132+
});
133+
this.posts = posts;
134+
},
135+
},
136+
};
137+
</script>
138+
139+
<style scoped lang="scss">
140+
.rss-wrapper {
141+
.meta-container {
142+
display: flex;
143+
align-items: center;
144+
text-decoration: none;
145+
margin: 0.25rem 0 0.5rem 0;
146+
p.feed-title {
147+
margin: 0;
148+
font-size: 1.2rem;
149+
font-weight: bold;
150+
color: var(--widget-text-color);
151+
}
152+
p.feed-author {
153+
margin: 0;
154+
font-size: 0.8rem;
155+
opacity: var(--dimming-factor);
156+
color: var(--widget-text-color);
157+
}
158+
img.feed-icon {
159+
border-radius: var(--curve-factor);
160+
width: 2rem;
161+
height: 2rem;
162+
margin-right: 0.5rem;
163+
}
164+
}
165+
166+
.post-row {
167+
border-top: 1px dashed var(--widget-text-color);
168+
padding: 0.5rem 0 0.25rem 0;
169+
.post-top {
170+
display: flex;
171+
align-items: center;
172+
text-decoration: none;
173+
.post-title-wrap {}
174+
p.post-title {
175+
margin: 0;
176+
font-size: 1rem;
177+
font-weight: bold;
178+
color: var(--widget-text-color);
179+
}
180+
p.post-date {
181+
font-size: 0.8rem;
182+
margin: 0;
183+
opacity: var(--dimming-factor);
184+
color: var(--widget-text-color);
185+
}
186+
img.post-img {
187+
border-radius: var(--curve-factor);
188+
width: 2rem;
189+
height: 2rem;
190+
margin-right: 0.5rem;
191+
}
192+
}
193+
.post-body {
194+
font-size: 0.85rem;
195+
color: var(--widget-text-color);
196+
max-height: 400px;
197+
overflow: hidden;
198+
::v-deep p {
199+
margin: 0.5rem 0;
200+
}
201+
::v-deep img {
202+
max-width: 80%;
203+
display: flex;
204+
margin: 0 auto;
205+
border-radius: var(--curve-factor);
206+
}
207+
::v-deep a {
208+
color: var(--widget-text-color);
209+
}
210+
::v-deep svg path {
211+
fill: var(--widget-text-color);
212+
}
213+
::v-deep blockquote {
214+
margin-left: 0.5rem;
215+
padding-left: 0.5rem;
216+
border-left: 4px solid var(--widget-text-color);
217+
}
218+
::v-deep .avatar.avatar-user { display: none; }
219+
}
220+
a.continue-reading-btn {
221+
width: 100%;
222+
display: block;
223+
font-size: 0.9rem;
224+
text-align: right;
225+
margin: 0 0 0.25rem;
226+
padding: 0.1rem 0.25rem;
227+
text-decoration: none;
228+
opacity: var(--dimming-factor);
229+
color: var(--widget-text-color);
230+
&:hover, &:focus {
231+
opacity: 1;
232+
text-decoration: underline;
233+
}
234+
}
235+
}
236+
}
237+
</style>

0 commit comments

Comments
 (0)