Adds refresh button to widget, for reloading data

This commit is contained in:
Alicia Sykes
2021-12-13 16:22:24 +00:00
parent ae8179ecd7
commit 2075cbc222
18 changed files with 254 additions and 49 deletions

View File

@@ -12,11 +12,11 @@
@openContextMenu="openContextMenu"
>
<!-- If no items, show message -->
<div v-if="(!items || items.length < 1) && !isEditMode" class="no-items">
<div v-if="sectionType === 'empty'" class="no-items">
{{ $t('home.no-items-section') }}
</div>
<!-- Item Container -->
<div v-else
<div v-else-if="sectionType === 'item'"
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''} inner-size-${itemSize}`"
:style="gridStyle" :id="`section-${groupId}`"
> <!-- Show for each item -->
@@ -57,6 +57,14 @@
:itemSize="itemSize"
/>
</div>
<div v-else-if="sectionType === 'widget'">
<WidgetBase
v-for="(widget, widgetIndx) in widgets"
:key="widgetIndx"
:widget="widget"
:index="index"
/>
</div>
<!-- Modal for opening in modal view -->
<IframeModal
:ref="`iframeModal-${groupId}`"
@@ -87,6 +95,7 @@
<script>
import router from '@/router';
import Item from '@/components/LinkItems/Item.vue';
import WidgetBase from '@/components/Widgets/WidgetBase';
import Collapsable from '@/components/LinkItems/Collapsable.vue';
import IframeModal from '@/components/LinkItems/IframeModal.vue';
import EditSection from '@/components/InteractiveEditor/EditSection.vue';
@@ -107,12 +116,14 @@ export default {
icon: String,
displayData: Object,
items: Array,
widgets: Array,
index: Number,
},
components: {
Collapsable,
ContextMenu,
Item,
WidgetBase,
IframeModal,
EditSection,
},
@@ -139,6 +150,12 @@ export default {
sortOrder() {
return this.displayData.sortBy || defaultSortOrder;
},
/* A section can contain either items or widgets */
sectionType() {
if (this.widgets && this.widgets.length > 0) return 'widget';
if (this.items && this.items.length > 0) return 'item';
return 'empty';
},
/* If the sortBy attribute is specified, then return sorted data */
sortedItems() {
let { items } = this;

View File

@@ -39,8 +39,12 @@ export default {
},
},
methods: {
update() {
this.setTime();
this.setDate();
},
/* Get and format the current time */
updateTime() {
setTime() {
this.time = Intl.DateTimeFormat(this.timeFormat, {
timeZone: this.timeZone,
hour: 'numeric',
@@ -49,7 +53,7 @@ export default {
}).format();
},
/* Get and format the date */
updateDate() {
setDate() {
this.date = new Date().toLocaleDateString(this.timeFormat, {
weekday: 'long', day: 'numeric', year: 'numeric', month: 'short',
});
@@ -57,14 +61,13 @@ export default {
},
created() {
// Set initial date and time
this.updateTime();
this.updateDate();
this.update();
// Update the date every hour, and the time each second
this.timeUpdateInterval = setInterval(() => {
this.updateTime();
this.setTime();
const now = new Date();
if (now.getMinutes() === 0 && now.getSeconds() === 0) {
this.updateDate();
this.setDate();
}
}, 1000);
},

View File

@@ -65,6 +65,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Create new chart, using the crypto data */
generateChart() {
return new Chart(`#${this.chartId}`, {

View File

@@ -75,6 +75,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@@ -52,6 +52,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@@ -1,6 +1,6 @@
<template>
<div class="iframe-widget">
<iframe v-if="frameUrl" :src="frameUrl" />
<iframe v-if="frameUrl" :src="frameUrl" :id="frameId" />
</div>
</template>
@@ -11,6 +11,7 @@ import ErrorHandler from '@/utils/ErrorHandler';
export default {
mixins: [WidgetMixin],
computed: {
/* Gets users specified URL to load into the iframe */
frameUrl() {
const usersChoice = this.options.url;
if (!usersChoice || typeof usersChoice !== 'string') {
@@ -19,6 +20,16 @@ export default {
}
return usersChoice;
},
/* Generates an ID for the iframe */
frameId() {
return `iframe-${btoa(this.frameUrl || 'empty').substring(0, 16)}`;
},
},
methods: {
/* Refreshes iframe contents, called by parent */
update() {
(document.getElementById(this.frameId) || {}).src = this.frameUrl;
},
},
};
</script>

View File

@@ -54,6 +54,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to Jokes API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@@ -72,6 +72,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Create new chart, using the crypto data */
generateChart() {
return new Chart(`#${this.chartId}`, {

View File

@@ -50,6 +50,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Makes GET request to the TFL API */
fetchData() {
axios.get(widgetApiEndpoints.tflStatus)

View File

@@ -1,5 +1,6 @@
<template>
<div class="weather">
<LoadingAnimation v-if="loading" class="loader" />
<div v-else class="weather">
<!-- Icon + Temperature -->
<div class="intro">
<p class="temp">{{ temp }}</p>
@@ -65,6 +66,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
processTemp(temp) {
return `${Math.round(temp)}${this.tempDisplayUnits}`;
@@ -73,6 +78,7 @@ export default {
fetchWeather() {
axios.get(this.endpoint)
.then((response) => {
this.loading = false;
const { data } = response;
this.icon = data.weather[0].icon;
this.description = data.weather[0].description;
@@ -141,6 +147,10 @@ export default {
<style scoped lang="scss">
@import '@/styles/weather-icons.scss';
.loader {
margin: 0 auto;
display: flex;
}
p {
color: var(--widget-text-color);
}

View File

@@ -75,6 +75,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
processTemp(temp) {
return `${Math.round(temp)}${this.tempDisplayUnits}`;

View File

@@ -1,19 +1,71 @@
<template>
<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" />
<CryptoPriceChart v-else-if="widgetType === 'crypto-price-chart'" :options="widgetOptions" />
<CryptoWatchList v-else-if="widgetType === 'crypto-watch-list'" :options="widgetOptions" />
<XkcdComic v-else-if="widgetType === 'xkcd-comic'" :options="widgetOptions" />
<ExchangeRates v-else-if="widgetType === 'exchange-rates'" :options="widgetOptions" />
<StockPriceChart v-else-if="widgetType === 'stock-price-chart'" :options="widgetOptions" />
<Jokes v-else-if="widgetType === 'joke'" :options="widgetOptions" />
<IframeWidget v-else-if="widgetType === 'iframe'" :options="widgetOptions" />
<div class="widget-base">
<Button :click="update" class="update-btn">
<UpdateIcon />
</Button>
<Clock
v-if="widgetType === 'clock'"
:options="widgetOptions"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
:ref="widgetRef"
/>
</div>
</template>
<script>
import ErrorHandler from '@/utils/ErrorHandler';
import Button from '@/components/FormElements/Button';
import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import Clock from '@/components/Widgets/Clock.vue';
import Weather from '@/components/Widgets/Weather.vue';
import WeatherForecast from '@/components/Widgets/WeatherForecast.vue';
@@ -29,6 +81,8 @@ import IframeWidget from '@/components/Widgets/IframeWidget.vue';
export default {
name: 'Widget',
components: {
Button,
UpdateIcon,
Clock,
Weather,
WeatherForecast,
@@ -46,9 +100,7 @@ export default {
index: Number,
},
computed: {
groupId() {
return `widget-${this.index}`;
},
/* Returns the widget type, shows error if not specified */
widgetType() {
if (!this.widget.type) {
ErrorHandler('Missing type attribute for widget');
@@ -56,14 +108,44 @@ export default {
}
return this.widget.type.toLowerCase();
},
/* Returns the users specified widget options, or empty object */
widgetOptions() {
return this.widget.options || {};
},
widgetRef() {
return `widget-${this.widgetType}-${this.index}`;
},
},
methods: {
update() {
this.$refs[this.widgetRef].update();
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
.widget-base {
position: relative;
padding-top: 0.75rem;
button.update-btn {
height: 1.5rem;
min-width: auto;
width: 2rem;
margin: 0;
padding: 0.1rem 0;
position: absolute;
right: -0.25rem;
top: -0.25rem;
border: none;
opacity: var(--dimming-factor);
color: var(--widget-text-color);
&:hover {
opacity: 1;
color: var(--widget-background-color);
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="widget-error"></div>
</template>
<script>
export default {
name: 'WidgetError',
props: {
errorMessage: String,
},
computed: {},
};
</script>
<style scoped lang="scss">
.widget-error {}
</style>

View File

@@ -40,11 +40,18 @@ export default {
}
return 'latest';
},
endpoint() {
return `${widgetApiEndpoints.xkcdComic}?comic=${this.comicNumber}`;
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(`${widgetApiEndpoints.xkcdComic}?comic=${this.comicNumber}`)
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data);
})