Adds a Sports Scores widget

This commit is contained in:
Alicia Sykes 2021-12-29 12:49:57 +00:00
parent a6b96483eb
commit 5684bf06e8
5 changed files with 429 additions and 9 deletions

View File

@ -2,6 +2,10 @@
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.
> **Note**: Widgets are still in the Alpha-phase of development.
> If you find a bug, please raise it.<br>
> Adding / editing widgets through the UI isn't yet supported, you will need to do this in the YAML config file.
##### Contents
- [General Widgets](#general-widgets)
- [Clock](#clock)
@ -13,6 +17,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [XKCD Comics](#xkcd-comics)
- [Code Stats](#code-stats)
- [Vulnerability Feed](#vulnerability-feed)
- [Sports Scores](#sports-scores)
- [Public Holidays](#public-holidays)
- [TFL Status](#tfl-status)
- [Exchange Rates](#exchange-rates)
@ -38,6 +43,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Iframe Widget](#iframe-widget)
- [HTML Embed Widget](#html-embedded-widget)
- [API Response](#api-response)
- [Prometheus Data](#prometheus-data)
- [Data Feed](#data-feed)
- [Usage & Customizations](#usage--customizations)
- [Widget Usage Guide](#widget-usage-guide)
@ -46,12 +52,13 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Language Translations](#language-translations)
- [Widget UI Options](#widget-ui-options)
- [Building a Widget](#build-your-own-widget)
- [Requesting a Widget](#requesting-a-widget)
## General Widgets
### Clock
A simple, live-updating time and date widget with time-zone support. All options are optional.
A simple, live-updating time and date widget with time-zone support. All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/vjb4RTv/clock.png" /></p>
@ -150,7 +157,7 @@ Displays the weather (temperature and conditions) for the next few days for a gi
### Crypto Watch List
Keep track of price changes of your favorite crypto assets. Data is fetched from [CoinGecko](https://www.coingecko.com/)
Keep track of price changes of your favorite crypto assets. Data is fetched from [CoinGecko](https://www.coingecko.com/). All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/WtS6jQ8/crypto-prices.png" /></p>
@ -265,7 +272,7 @@ Display news and updates from any RSS-enabled service.
### XKCD Comics
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.
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. All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/kqV68hy/xkcd-comic.png" /></p>
@ -328,7 +335,7 @@ Display your coding summary. [Code::Stats](https://codestats.net/) is a free and
### Vulnerability Feed
Display a feed of recent vulnerabilities, with optional filtering by score, exploits, vendor and product. All fields are optional.
Keep track of recent security advisories and vulnerabilities, with optional filtering by score, exploits, vendor and product. All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/DYJMpjp/vulnerability-feed.png" /></p>
@ -371,6 +378,39 @@ or
---
### Sports Scores
Show recent scores and upcoming matches from your favourite sports team. Data is fetched from [TheSportsDB.com](https://www.thesportsdb.com/). From the UI, you can click any other team to view their scores and upcoming games, or click a league name to see all teams.
<p align="center"><img width="400" src="https://i.ibb.co/8XhXGkN/sports-scores.png" /></p>
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`teamId`** | `string` | __Optional__ | The ID of a team to fetch scores from. You can search for your team on the [Teams Page](https://www.thesportsdb.com/teams_main.php)
**`leagueId`** | `string` | __Optional__ | Alternatively, provide a league ID to fetch all games from. You can find the ID on the [Leagues Page](https://www.thesportsdb.com/Sport/Leagues)
**`pastOrFuture`** | `string` | __Optional__ | Set to `past` to show scores for recent games, or `future` to show upcoming games. Defaults to `past`. You can change this within the UI
**`apiKey`** | `string` | __Optional__ | Optionally specify your API key, which you can sign up for at [TheSportsDB.com](https://www.thesportsdb.com/)
**`limit`** | `number` | __Optional__ | To limit output to a certain number of matches, defaults to `15`
##### Example
```yaml
- type: sports-scores
options:
teamId: 133636
```
##### Info
- **CORS**: 🟢 Enabled
- **Auth**: 🟠 Optional
- **Price**: 🟠 Free plan (upto 30 requests / second, limited endpoints)
- **Host**: Managed Instance Only
- **Privacy**: ⚫ No Policy Available
---
### Public Holidays
Counting down to the next day off work? This widget displays upcoming public holidays for your country. Data is fetched from [Enrico](http://kayaposoft.com/enrico/)
@ -406,7 +446,7 @@ Counting down to the next day off work? This widget displays upcoming public hol
### TFL Status
Shows real-time tube status of the London Underground. All options are optional.
Shows real-time tube status of the London Underground. All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/LRDhXDn/tfl-status.png" /></p>
@ -517,7 +557,7 @@ Shows recent price history for a given publicly-traded stock or share
### Joke
Renders a programming or generic joke. Data is fetched from the [JokesAPI](https://github.com/Sv443/JokeAPI) by @Sv443
Renders a programming or generic joke. Data is fetched from the [JokesAPI](https://github.com/Sv443/JokeAPI) by @Sv443. All fields are optional.
<p align="center"><img width="400" src="https://i.ibb.co/sQJGkyR/joke.png" /></p>
@ -645,7 +685,7 @@ _No config options._
### GitHub Trending
Displays currently trending projects on GitHub. Optionally specify a language and time-frame. Data is fetched from [Lissy93/gh-trending-no-cors](https://github.com/Lissy93/gh-trending-no-cors) using the GitHub API.
Displays currently trending projects on GitHub. Optionally specify a language and time-frame. Data is fetched from [Lissy93/gh-trending-no-cors](https://github.com/Lissy93/gh-trending-no-cors) using the GitHub API. All fields are optional.
<p align="center"><img width="380" src="https://i.ibb.co/BGy7Q3g/github-trending.png" /></p>
@ -1079,7 +1119,17 @@ Or
### API Response
Directly output plain-text response from any API-enabled service
Directly output plain-text response from any API-enabled service.
// Coming soon...
---
### Prometheus Data
Display data from any service with a Prometheus exporter.
// Coming soon...
---
@ -1185,4 +1235,23 @@ Widgets cannot currently be edited through the UI. This feature is in developmen
Widgets are built in a modular fashion, making it easy for anyone to create their own custom components.
For a full tutorial on creating your own widget, you can follow [this guide](https://github.com/Lissy93/dashy/blob/master/docs/development-guides.md#building-a-widget), or take a look at [here](https://github.com/Lissy93/dashy/commit/3da76ce2999f57f76a97454c0276301e39957b8e) for a code example.
For a full tutorial on creating your own widget, you can follow [this guide](/docs/development-guides.md#building-a-widget), or take a look at [here](https://github.com/Lissy93/dashy/commit/3da76ce2999f57f76a97454c0276301e39957b8e) for a code example.
Alternatively, for displaying simple data, you could also just use the either the [iframe](#iframe-widget), [embed](#html-embedded-widget), [Data Feed](#data-feed) or [API response](#api-response) widgets.
---
### Requesting a Widget
Suggestions for widget ideas are welcome. But there is no guarantee that I will build your widget idea.
You can suggest a widget [here](https://git.io/Jygo3), please star the repo before submitting a ticket.
Please only request widgets for services that:
- Have a publicly accessible API
- Are CORS and HTTPS enabled
- Are free to use, or have a free plan
- Allow for use in their Terms of Service
- Would be useful for other users
For services that are not officially supported, it is likely still possible to display data using either the [iframe](#iframe-widget), [embed](#html-embedded-widget) or [API response](#api-response) widgets. For more advanced features, like charts and action buttons, you could also build your own widget, using [this tutorial](/docs/development-guides.md#building-a-widget), it's fairly straight forward, and you can use an [existing widget](https://github.com/Lissy93/dashy/tree/master/src/components/Widgets) (or [this example](https://git.io/JygKI)) as a template.

View File

@ -0,0 +1,337 @@
<template>
<div class="sports-scores-wrapper" v-if="matches">
<!-- Show back to original button -->
<p v-if="whatToShow === 'team' && currentTeamId !== teamId"
@click="fetchTeamScores(teamId)" class="back-to-original">
Back to Original Team
</p>
<p v-else-if="whatToShow === 'league' && leagueId && currentLeagueId !== leagueId"
@click="fetchLeagueScores(leagueId)" class="back-to-original">
Back to Original League
</p>
<!-- Show toggle switch for past and future matches -->
<div class="past-or-future">
<span
:class="`btn ${whenToShow === 'past' ? 'selected' : ''}`"
v-tooltip="tooltip('View Recent Scores')"
@click="fetchPastFutureEvents('past')"
>
Past Scores
</span>
<span
:class="`btn ${whenToShow === 'future' ? 'selected' : ''}`"
v-tooltip="tooltip('View Upcoming Games')"
@click="fetchPastFutureEvents('future')"
>
Upcoming Games
</span>
</div>
<div class="match-row" v-for="match in matches" :key="match.id">
<!-- Banner Image -->
<div class="match-thumbnail-wrap">
<img :src="match.thumbnail" :alt="`${match.title} Banner Image`" class="match-thumbnail" />
</div>
<!-- Team Scores -->
<div class="score">
<div
:class="`score-block home ${currentTeamId !== match.home.id ? 'clickable' : ''}`"
v-tooltip="tooltip(`Click to view ${match.home.name} Scores`)"
@click="fetchTeamScores(match.home.id)"
>
<p class="team-score">{{ match.home.score }}</p>
<p class="team-name">{{ match.home.name }}</p>
<p class="team-location">Home</p>
</div>
<div class="colon">{{ match.home.score || match.away.score ? ':' : 'v' }}</div>
<div
class="score-block away clickable"
v-tooltip="tooltip(`Click to view ${match.away.name} Scores`)"
@click="fetchTeamScores(match.away.id)"
>
<p class="team-score">{{ match.away.score }}</p>
<p class="team-name">{{ match.away.name }}</p>
<p class="team-location">Away</p>
</div>
</div>
<!-- Match Meta Info -->
<div class="match-info">
<p class="status">{{ match.status }} </p>
<p class="league" @click="fetchLeagueScores(match.leagueId)">
{{ match.league }}, {{ match.season }}
</p>
<p>
<a :href="match.venue | mapsUrl">{{ match.venue }}</a>
on {{ match.date | formatDate }} ({{ match.time | formatTime }})</p>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import { timestampToDate, getPlaceUrl } from '@/utils/MiscHelpers';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
mixins: [WidgetMixin],
data() {
return {
currentTeamId: null, // ID of the selected team
currentLeagueId: null, // ID of selected league
whenToShow: null, // Either 'past' or 'future'
whatToShow: null, // Either 'team' or 'league'
matches: null, // Array of matches returned
initiated: false, // Set to true once values set
};
},
computed: {
teamId() {
return this.options.teamId;
},
leagueId() {
return this.options.leagueId;
},
apiKey() {
return this.options.apiKey || '50130162';
},
limit() {
return this.options.limit || 20;
},
pastOrFuture() {
return this.options.pastOrFuture || 'past';
},
endpoint() {
this.initiate();
const endpoint = widgetApiEndpoints.sportsScores;
if (this.whatToShow === 'league' && this.whenToShow === 'future') {
return `${endpoint}/${this.apiKey}/eventsnextleague.php?id=${this.currentLeagueId}`;
} else if (this.whatToShow === 'league' && this.whenToShow === 'past') {
return `${endpoint}/${this.apiKey}/eventspastleague.php?id=${this.currentLeagueId}`;
} else if (this.whatToShow === 'team' && this.whenToShow === 'future') {
return `${endpoint}/${this.apiKey}/eventsnext.php?id=${this.currentTeamId}`;
} else if (this.whatToShow === 'team' && this.whenToShow === 'past') {
return `${endpoint}/${this.apiKey}/eventslast.php?id=${this.currentTeamId}`;
} else {
this.error('Missing team or league ID');
return '';
}
},
},
filters: {
formatDate(dateStr) {
return timestampToDate(dateStr);
},
formatTime(timeStr) {
if (!timeStr) return '';
return timeStr.slice(0, 5);
},
mapsUrl(placeName) {
return getPlaceUrl(placeName);
},
},
methods: {
initiate() {
if (!this.initiated) {
this.currentTeamId = this.teamId;
this.currentLeagueId = this.leagueId;
this.whenToShow = this.pastOrFuture;
this.whatToShow = this.teamOrLeague();
this.initiated = true;
}
},
fetchData() {
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data.results || response.data.events);
})
.catch((dataFetchError) => {
this.error('Unable to fetch data', dataFetchError);
this.finishLoading();
})
.finally(() => {
this.finishLoading();
});
},
processData(data) {
const matches = [];
data.forEach((match) => {
matches.push({
id: match.idEvent,
sport: match.strSport,
title: match.strEvent,
league: match.strLeague,
leagueId: match.idLeague,
season: match.strSeason,
venue: match.strVenue,
date: match.dateEvent,
time: match.strTime,
status: match.strStatus,
thumbnail: match.strThumb,
home: {
id: match.idHomeTeam,
name: match.strHomeTeam,
score: match.intHomeScore,
},
away: {
id: match.idAwayTeam,
name: match.strAwayTeam,
score: match.intAwayScore,
},
});
});
this.matches = matches.slice(0, this.limit);
},
teamOrLeague() {
if (!this.currentTeamId && !this.currentLeagueId) {
this.error('You must specify either a teamId or leagueId');
}
if (this.currentTeamId) return 'team';
return 'league';
},
fetchTeamScores(teamId) {
if (teamId) {
this.whatToShow = 'team';
this.startLoading();
this.currentTeamId = teamId;
this.fetchData();
}
},
fetchLeagueScores(leagueId) {
if (leagueId) {
this.whatToShow = 'league';
this.startLoading();
this.currentLeagueId = leagueId;
this.fetchData();
}
},
fetchPastFutureEvents(pastOrFuture) {
this.startLoading();
this.whenToShow = pastOrFuture;
this.fetchData();
},
tooltip(content) {
return {
content, html: true, trigger: 'hover focus', delay: 250,
};
},
},
};
</script>
<style scoped lang="scss">
.sports-scores-wrapper {
p {
font-size: 1rem;
margin: 0.5rem auto;
color: var(--widget-text-color);
}
.match-row {
.match-thumbnail-wrap {
width: 80%;
max-height: 5rem;
display: flex;
border-radius: var(--curve-factor);
margin: 1rem auto 0.5rem auto;
overflow: hidden;
img.match-thumbnail {
width: 100%;
height: fit-content;
margin-top: -13%;
}
}
.score {
display: flex;
justify-content: space-around;
.score-block {
display: flex;
flex-direction: column;
min-width: 40%;
border: 1px solid transparent;
border-radius: var(--curve-factor);
p.team-score {
margin: 0.25rem auto;
font-size: 1.5rem;
font-weight: bold;
font-family: var(--font-monospace);
}
p.team-name {
text-align: center;
margin: 0;
}
p.team-location {
font-size: 0.8rem;
margin: 0 auto;
opacity: var(--dimming-factor);
}
&.clickable {
cursor: pointer;
&:hover {
border: 1px dashed var(--widget-text-color);
}
}
}
.colon {
margin: 0;
font-size: 2rem;
font-weight: bold;
color: var(--widget-text-color);
}
}
.match-info {
background: var(--widget-accent-color);
border-radius: var(--curve-factor);
padding: 0.25rem 0.5rem;
margin: 0.5rem auto 1rem auto;
p, a {
color: var(--widget-text-color);
opacity: var(--dimming-factor);
font-size: 0.8rem;
margin: 0;
&.status {
font-weight: bold;
}
&.league {
text-decoration: underline;
cursor: pointer;
}
}
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
p.back-to-original {
cursor: pointer;
font-size: 1rem;
padding: 0.1rem 0.25rem;
width: 100%;
color: var(--widget-text-color);
border-radius: var(--curve-factor);
text-decoration: underline;
text-align: left;
}
.past-or-future {
width: 100%;
color: var(--widget-text-color);
border-bottom: 1px dashed var(--widget-text-color);
padding: 0.5rem 0;
display: flex;
justify-content: space-evenly;
span.btn {
max-width: 50%;
cursor: pointer;
padding: 0.1rem 0.25rem;
border-radius: var(--curve-factor);
&.selected {
background: var(--widget-text-color);
color: var(--widget-background-color);
}
&:hover {
font-weight: bold;
}
}
}
}
</style>

View File

@ -186,6 +186,13 @@
@error="handleError"
:ref="widgetRef"
/>
<SportsScores
v-else-if="widgetType === 'sports-scores'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StatPing
v-else-if="widgetType === 'stat-ping'"
:options="widgetOptions"
@ -282,6 +289,7 @@ export default {
PublicHolidays: () => import('@/components/Widgets/PublicHolidays.vue'),
PublicIp: () => import('@/components/Widgets/PublicIp.vue'),
RssFeed: () => import('@/components/Widgets/RssFeed.vue'),
SportsScores: () => import('@/components/Widgets/SportsScores.vue'),
StatPing: () => import('@/components/Widgets/StatPing.vue'),
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),

View File

@ -113,6 +113,11 @@ export const getMapUrl = (location, zoom) => {
return `https://www.openstreetmap.org/#map=${zoom || 10}/${location.lat}/${location.lon}`;
};
/* Given a place name, return a link to Google Maps search page */
export const getPlaceUrl = (placeName) => {
return `https://www.google.com/maps/search/${encodeURIComponent(placeName)}`;
};
/* Given a large number, will add commas to make more readable */
export const putCommasInBigNum = (bigNum) => {
const strNum = Number.isNaN(bigNum) ? bigNum : String(bigNum);

View File

@ -222,6 +222,7 @@ module.exports = {
publicIp: 'http://ip-api.com/json',
readMeStats: 'https://github-readme-stats.vercel.app/api',
rssToJson: 'https://api.rss2json.com/v1/api.json',
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
stockPriceChart: 'https://www.alphavantage.co/query',
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
weather: 'https://api.openweathermap.org/data/2.5/weather',