Adds a London Underground status widget

This commit is contained in:
Alicia Sykes 2021-12-10 13:17:48 +00:00
parent c38a094a63
commit cf7e021a82
5 changed files with 231 additions and 15 deletions

View File

@ -86,6 +86,33 @@ Displays the weather (temperature and conditions) for the next few days for a gi
units: imperial
```
### TFL Status
Shows real-time tube status of the London Underground. All options are optional.
##### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`showAll`** | `boolean` | _Optional_ | By default, details for lines with a Good Service are not visible, but you can click More Details to see all. Setting this option to `true` will show all lines on initial page load
**`sortAlphabetically`** | `boolean` | _Optional_ | By default lines are sorted by current status, set this option to `true` to instead sort them alphabetically
**`linesToShow`** | `array` | _Optional_ | By default all lines are shown. If you're only interested in the status of a few lines, then pass in an array of lines to show, specified by name
##### Example
```yaml
- name: TFL Status
icon: '🚋'
type: tfl-status
options:
showAll: true
sortAlphabetically: true
linesToShow:
- District
- Jubilee
- Central
```
---
## Dynamic Widgets

View File

@ -1,7 +1,8 @@
{
"home": {
"no-results": "No Search Results",
"no-data": "No Data Configured"
"no-data": "No Data Configured",
"no-items-section": "No Items to Show Yet"
},
"search": {
"search-label": "Search",
@ -243,5 +244,19 @@
"download-file-tooltip": "Download all app config to your device, in a YAML file",
"view-title": "View Config"
}
},
"widgets": {
"general": {
"loading": "Loading...",
"show-more": "Expand Details",
"show-less": "Show Less"
},
"clock": {},
"weather": {},
"weather-forecast": {},
"tfl-status": {
"good-service-all": "Good Service on all Lines",
"good-service-rest": "Good Service on all other Lines"
}
}
}

View File

@ -0,0 +1,170 @@
<template>
<div class="tfl-status">
<template v-if="lineStatuses">
<div v-for="line in filterLines" :key="line.index" class="line-row">
<p class="row name">{{ line.line }}</p>
<p :class="`row status ${getStatusColor(line.statusCode)}`">{{ line.status }}</p>
<p class="row disruption" v-if="line.disruption">{{ line.disruption | format }}</p>
</div>
<div v-if="!showAll" class="line-row">
<p class="row all-other">
{{
filterLines.length > 0 ?
$t('widgets.tfl-status.good-service-rest') :
$t('widgets.tfl-status.good-service-all')
}}
</p>
</div>
<p class="more-details-btn" @click="toggleAllLines">
{{ showAll ? $t('widgets.general.show-less') : $t('widgets.general.show-more') }}
</p>
</template>
</div>
</template>
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import ErrorHandler from '@/utils/ErrorHandler';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
mixins: [WidgetMixin],
data() {
return {
lineStatuses: null,
showAll: false,
};
},
computed: {
/* Return only the lines without a good service, unless showing all */
filterLines() {
if (this.showAll) { return this.lineStatuses; }
return this.lineStatuses.filter((line) => line.statusCode !== 10);
},
},
filters: {
format(description) {
const parts = description.split(':');
return parts.length > 1 ? parts[1] : parts[0];
},
},
methods: {
/* Makes GET request to the TFL API */
fetchData() {
axios.get(widgetApiEndpoints.tflStatus)
.then((response) => {
this.lineStatuses = this.processData(response.data);
})
.catch(() => {
ErrorHandler('Unable to fetch data from TFL API');
});
},
/* Processes the results to be rendered by the UI */
processData(data) {
let results = [];
data.forEach((line, index) => {
results.push({
index,
line: line.name,
statusCode: line.lineStatuses[0].statusSeverity,
status: line.lineStatuses[0].statusSeverityDescription,
disruption: line.lineStatuses[0].reason,
});
});
if (!this.options.sortAlphabetically) {
results = this.sortByStatusCode(results);
}
if (this.options.linesToShow && Array.isArray(this.options.linesToShow)) {
results = this.filterByLineName(results, this.options.linesToShow);
}
return results;
},
/* Get color, depending on the status code */
getStatusColor(code) {
if (code <= 6) return 'red';
if (code <= 9) return 'orange';
return 'green';
},
/* If user only wants to see results from certain lines, filter the rest out */
filterByLineName(allLines, usersLines) {
const chosenLines = usersLines.map(name => name.toLowerCase());
const filtered = allLines.filter((line) => chosenLines.includes(line.line.toLowerCase()));
if (filtered.length < 1) {
ErrorHandler('No TFL lines match your filter');
return allLines;
}
return filtered;
},
/* Sort results in order of most-delayed first */
sortByStatusCode(lines) {
return lines.reverse().sort((a, b) => (a.statusCode > b.statusCode ? 1 : -1));
},
/* Toggle show/ hide all lines */
toggleAllLines() {
this.showAll = !this.showAll;
},
},
created() {
if (this.options.showAll) this.showAll = true;
this.fetchData();
},
};
</script>
<style scoped lang="scss">
.tfl-status {
.line-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 0.5rem 0.25rem;
.row {
margin: 0.2rem 0;
}
.status {
font-weight: bold;
text-align: right;
&.green { color: var(--success); }
&.orange { color: var(--warning); }
&.red { color: var(--danger); }
}
.disruption {
opacity: var(--dimming-factor);
font-size: 0.85rem;
grid-column-start: span 2;
}
.all-other {
grid-column-start: span 2;
font-weight: bold;
text-align: center;
color: var(--success)
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
p {
color: var(--widget-text-color);
cursor: default;
margin: 0;
}
// Show more details button
.more-details-btn {
cursor: pointer;
text-align: center;
margin: 0.5rem 0.25rem 0.25rem;
padding: 0.1rem 0.25rem;
border: 1px solid transparent;
border-radius: var(--curve-factor);
&:hover {
border: 1px solid var(--widget-text-color);
}
&:focus, &:active {
background: var(--widget-text-color);
color: var(--widget-background-color);
}
}
}
</style>

View File

@ -1,19 +1,20 @@
<template>
<div>
<Collapsable
:title="widget.name"
:icon="widget.icon"
:uniqueKey="groupId"
:collapsed="displayData.collapsed"
:cols="displayData.cols"
:rows="displayData.rows"
:color="displayData.color"
:customStyles="displayData.customStyles"
>
<Clock v-if="widgetType === 'clock'" :options="widgetOptions" />
<Weather v-else-if="widgetType === 'weather'" :options="widgetOptions" />
<WeatherForecast v-else-if="widgetType === 'weather-forecast'" :options="widgetOptions" />
</Collapsable>
<Collapsable
:title="widget.name"
:icon="widget.icon"
:uniqueKey="groupId"
:collapsed="displayData.collapsed"
:cols="displayData.cols"
:rows="displayData.rows"
:color="displayData.color"
:customStyles="displayData.customStyles"
>
<Clock v-if="widgetType === 'clock'" :options="widgetOptions" />
<Weather v-else-if="widgetType === 'weather'" :options="widgetOptions" />
<WeatherForecast v-else-if="widgetType === 'weather-forecast'" :options="widgetOptions" />
<TflStatus v-else-if="widgetType === 'tfl-status'" :options="widgetOptions" />
</Collapsable>
</div>
</template>
@ -21,6 +22,7 @@
import Clock from '@/components/Widgets/Clock.vue';
import Weather from '@/components/Widgets/Weather.vue';
import WeatherForecast from '@/components/Widgets/WeatherForecast.vue';
import TflStatus from '@/components/Widgets/TflStatus.vue';
import Collapsable from '@/components/LinkItems/Collapsable.vue';
export default {
@ -30,6 +32,7 @@ export default {
Clock,
Weather,
WeatherForecast,
TflStatus,
},
props: {
widget: Object,

View File

@ -207,6 +207,7 @@ module.exports = {
widgetApiEndpoints: {
weather: 'https://api.openweathermap.org/data/2.5/weather',
weatherForecast: 'https://api.openweathermap.org/data/2.5/forecast/daily',
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
},
/* URLs for web search engines */
searchEngineUrls: {