Compare commits

..

No commits in common. "master" and "v0.1.5" have entirely different histories.

16 changed files with 261 additions and 549 deletions

1
.gitignore vendored
View file

@ -28,4 +28,3 @@ coverage
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
*~

View file

@ -7,34 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
## 0.1.11 - 2025-10-06
### Changed
- update source code info for about page
## 0.1.10 - 2024-06-09
### Changed
- Fix URL bug when refreshing weather radar.
## 0.1.9 - 2024-06-09
### Changed
- Warning notification should not butt up against forecast panel.
- Add timestamp param to bust cache when refreshing radar images.
## 0.1.8 - 2024-06-09
### Added
- Add link to national radar map (live image).
- Add refresh buttons to weather data pages.
### Changed
- Convert all view components to use Composition API.
## 0.1.7 - 2024-06-08
### Added
- Add basic support for active weather alerts.
## 0.1.6 - 2024-06-08
### Changed
- Add proper About page.
## 0.1.5 - 2024-06-08 ## 0.1.5 - 2024-06-08
### Changed ### Changed
- Tighten up the forecast period panels a bit. - Tighten up the forecast period panels a bit.

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "myweather", "name": "myweather",
"version": "0.1.11", "version": "0.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "myweather", "name": "myweather",
"version": "0.1.11", "version": "0.1.5",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "myweather", "name": "myweather",
"version": "0.1.11", "version": "0.1.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View file

@ -3,12 +3,12 @@ import { RouterLink, RouterView } from 'vue-router'
</script> </script>
<template> <template>
<div class="page-wrapper">
<header> <header>
<router-link to="/"> <router-link to="/">
<img src="/nwslogo.gif" width="38" height="38" /> <img src="/nwslogo.gif" width="38" height="38" />
<h5 class="is-size-5 has-text-weight-bold">Weather</h5> <h5 class="is-size-5 has-text-weight-bold">Weather</h5>
</router-link> </router-link>
</header> </header>
<div style="padding: 0.5rem;"> <div style="padding: 0.5rem;">
@ -18,13 +18,6 @@ import { RouterLink, RouterView } from 'vue-router'
</keep-alive> </keep-alive>
</router-view> </router-view>
</div> </div>
</div>
<footer class="footer">
<div class="content">
<router-link to="/about">About this app</router-link>
</div>
</footer>
</template> </template>
<style scoped> <style scoped>

View file

@ -1,10 +0,0 @@
import packageData from '../package.json'
const appsettings = {
appTitle: packageData.name,
appVersion: packageData.version,
appDependencies: packageData.dependencies,
}
export default appsettings

View file

@ -1,15 +1 @@
@import './custom.scss'; @import './custom.scss';
/* nb. these are here to force footer to bottom of screen */
html, body {
height: 100%;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}

View file

@ -1,7 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import WeatherView from '../views/WeatherView.vue' import WeatherView from '../views/WeatherView.vue'
import AlertsView from '../views/AlertsView.vue'
import HourlyView from '../views/HourlyView.vue' import HourlyView from '../views/HourlyView.vue'
import EditListView from '../views/EditListView.vue' import EditListView from '../views/EditListView.vue'
@ -18,11 +17,6 @@ const router = createRouter({
name: 'weather', name: 'weather',
component: WeatherView, component: WeatherView,
}, },
{
path: '/alerts',
name: 'alerts',
component: AlertsView,
},
{ {
path: '/hourly', path: '/hourly',
name: 'hourly', name: 'hourly',

View file

@ -10,7 +10,6 @@ const getDefaults = () => {
coordinates, coordinates,
cityState, cityState,
weather: null, weather: null,
alerts: null,
forecast: null, forecast: null,
radarLatestURL: null, radarLatestURL: null,
radarLoopURL: null, radarLoopURL: null,
@ -26,26 +25,21 @@ export const useWeatherStore = defineStore('weather', {
actions: { actions: {
clearWeather(keepCoordinates) { clearWeather() {
if (!keepCoordinates) {
this.setCoordinates(null) this.setCoordinates(null)
this.setCityState(null) this.setCityState(null)
}
this.setWeather(null) this.setWeather(null)
this.setForecast(null) this.setForecast(null)
this.radarLatestURL = null this.radarLatestURL = null
this.radarLoopURL = null this.radarLoopURL = null
this.alerts = null
}, },
async getWeather() { async getWeather() {
if (!this.weather) { if (!this.weather) {
let url
let response
url = `https://api.weather.gov/points/${this.coordinates}` const url = `https://api.weather.gov/points/${this.coordinates}`
response = await fetch(url) const response = await fetch(url)
const weather = await response.json() const weather = await response.json()
if (weather.status == 404) { if (weather.status == 404) {
throw new Error(`Data not found for ${this.coordinates}`) throw new Error(`Data not found for ${this.coordinates}`)
@ -63,65 +57,6 @@ export const useWeatherStore = defineStore('weather', {
this.radarLoopURL = `https://radar.weather.gov/ridge/standard/${station}_loop.gif` this.radarLoopURL = `https://radar.weather.gov/ridge/standard/${station}_loop.gif`
this.setWeather(weather) this.setWeather(weather)
// fetch zone to get its official id
url = weather.properties.forecastZone
response = await fetch(url)
const zone = await response.json()
// fetch alerts for zone
url = `https://api.weather.gov/alerts/active/zone/${zone.properties.id}`
response = await fetch(url)
const zoneAlerts = await response.json()
// fetch county to get its official id
url = weather.properties.county
response = await fetch(url)
const county = await response.json()
// fetch alerts for county
url = `https://api.weather.gov/alerts/active/zone/${county.properties.id}`
response = await fetch(url)
const countyAlerts = await response.json()
const newAlerts = {}
// use latest timestamp from either zone or county
newAlerts.updated = zoneAlerts.updated
if (countyAlerts.updated > zoneAlerts.updated) {
newAlerts.updated = countyAlerts.updated
}
// collect all alert "features" but de-duplicate them
newAlerts.features = {}
for (let feature of zoneAlerts.features) {
newAlerts.features[feature.properties.id] = feature
}
for (let feature of countyAlerts.features) {
newAlerts.features[feature.properties.id] = feature
}
newAlerts.features = Object.values(newAlerts.features)
// put "likely" before "possible" alerts
newAlerts.features.sort((a, b) => {
if (a.properties.certainty == 'Likely' && b.properties.certainty != 'Likely') {
return -1
}
if (a.properties.certainty != 'Likely' && b.properties.certainty == 'Likely') {
return 1
}
// if (a.properties.certainty == b.properties.certainty) {
// return 0
// }
// TODO: what else should this do?
return 0
})
// we have our final alerts
this.alerts = newAlerts
} }
return this.weather return this.weather
@ -131,7 +66,7 @@ export const useWeatherStore = defineStore('weather', {
if (!this.forecast) { if (!this.forecast) {
const weather = await this.getWeather() const weather = await this.getWeather(this.coordinates)
const url = weather.properties.forecast const url = weather.properties.forecast
const response = await fetch(url) const response = await fetch(url)

View file

@ -1,57 +1,15 @@
<script setup>
import appsettings from '../appsettings'
</script>
<template> <template>
<div class="about"> <div class="about">
<h4 class="is-size-4">{{ appsettings.appTitle }} {{ appsettings.appVersion }}</h4> <h1>This is an about page</h1>
<br />
<p class="block">
This app was built in rather a hurry, to replace my personal
usage of the original
<a href="https://mobile.weather.gov" target="_blank">
<span class="icon-text">
<o-icon icon="external-link" />
<span>mobile.weather.gov</span>
</span>
</a>
site which was being decommissioned.&nbsp;
(See also
<a href="https://www.weather.gov/media/notification/pdf_2023_24/scn24-51_mobile_decommissioning.pdf" target="_blank">
<span class="icon-text">
<o-icon icon="external-link" />
<span>this statement</span>
</span>
</a>.)
</p>
<p class="block">
Source code is available at
<a href="https://forgejo.wuttaproject.org/lance/myweather" target="_blank">
<span class="icon-text">
<o-icon icon="external-link" />
<span>https://forgejo.wuttaproject.org/lance/myweather</span>
</span>
</a>
</p>
<p class="block">
This app is primarily for personal use, but if you also find it useful and
have suggestions, email me at <a href="mailto:lance@edbob.org">lance@edbob.org</a>
</p>
<br />
<o-field label="Dependencies">
<ul>
<li v-for="(ver, pkg, i) in appsettings.appDependencies"
:key="pkg">
{{ pkg }} {{ ver }}
</li>
</ul>
</o-field>
</div> </div>
</template> </template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View file

@ -1,65 +0,0 @@
<script setup>
import { ref, onActivated } from 'vue'
import { useWeatherStore } from '../stores/weather'
const weatherStore = useWeatherStore()
const refreshing = ref(false)
onActivated(() => {
refreshing.value = true
weatherStore.getWeather().then(() => {
refreshing.value = false
})
})
async function refreshAlerts() {
refreshing.value = true
weatherStore.clearWeather(true)
await weatherStore.getWeather()
refreshing.value = false
}
</script>
<template>
<main>
<div class="block"
style="display: flex; align-items: center; justify-content: space-between;">
<o-button variant="primary"
icon-left="arrow-left"
@click="$router.push('/weather')">
Back
</o-button>
<o-button variant="primary"
icon-left="refresh"
@click="refreshAlerts()"
:disabled="refreshing">
{{ refreshing ? "Refreshing" : "Refresh" }}
</o-button>
</div>
<h5 class="is-size-5">{{ weatherStore.cityState }}</h5>
<h5 class="is-size-5">Alerts</h5>
<br />
<o-collapse v-for="feature in weatherStore.alerts?.features || []"
variant="warning"
:open="false">
<template #trigger>
<o-notification :variant="feature.properties.certainty == 'Likely' ? 'danger' : 'warning'"
icon="plus"
icon-size="small"
style="cursor: pointer;">
{{ feature.properties.event }} ({{ feature.properties.severity }}, {{ feature.properties.certainty }})
</o-notification>
</template>
<div class="notification">
<h5 class="is-size-5 block">{{ feature.properties.headline }}</h5>
<p class="block" style="white-space: pre-wrap;">{{ feature.properties.description }}</p>
</div>
</o-collapse>
</main>
</template>

View file

@ -1,12 +1,22 @@
<script setup> <script setup>
import { mapStores } from 'pinia'
import { useLocationStore } from '../stores/location' import { useLocationStore } from '../stores/location'
</script>
const locationStore = useLocationStore() <script>
export default {
function deleteLocation(location) { computed: {
locationStore.removeLocation(location) ...mapStores(useLocationStore),
},
methods: {
deleteLocation(location) {
this.locationStore.removeLocation(location)
},
},
} }
</script> </script>
<template> <template>

View file

@ -1,75 +1,77 @@
<script setup> <script setup>
import { ref } from 'vue' import { mapStores } from 'pinia'
import { useRouter } from 'vue-router'
import { useWeatherStore } from '../stores/weather' import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location' import { useLocationStore } from '../stores/location'
import { useOruga } from '@oruga-ui/oruga-next' </script>
<script>
export default {
const router = useRouter() data() {
const locationStore = useLocationStore() return {
const weatherStore = useWeatherStore() coordinates: null,
const oruga = useOruga() loading: false,
locationAccessBlocked: false,
}
},
const coordinates = ref(null) computed: {
const coordinatesInput = ref() ...mapStores(useWeatherStore, useLocationStore),
const loading = ref(false) },
const locationAccessBlocked = ref(false)
methods: {
function useCurrentLocation() { useCurrentLocation() {
if (locationAccessBlocked.value) { if (this.locationAccessBlocked) {
alert("You must refresh the page first, then try again.") alert("You must refresh the page first, then try again.")
return return
} }
navigator.geolocation.getCurrentPosition(loc => { navigator.geolocation.getCurrentPosition(loc => {
coordinates.value = `${loc.coords.latitude},${loc.coords.longitude}` this.coordinates = `${loc.coords.latitude},${loc.coords.longitude}`
loadCoordinates() this.loadCoordinates()
}, error => { }, error => {
if (error.code == 1) { // PERMISSION_DENIED if (error.code == 1) { // PERMISSION_DENIED
locationAccessBlocked.value = true this.locationAccessBlocked = true
} }
alert(`error.code = ${error.code}\n\n${error.message}`) alert(`error.code = ${error.code}\n\n${error.message}`)
}) })
} },
editLocations() {
this.$router.push('/edit-list')
},
function editLocations() { async loadCoordinates() {
router.push('/edit-list') if (!this.coordinates) {
} this.$refs.coordinates.focus()
async function loadCoordinates() {
if (!coordinates.value) {
coordinatesInput.value.focus()
return return
} }
const parts = coordinates.value.split(/(?: +| *\, *)/) const parts = this.coordinates.split(/(?: +| *\, *)/)
const pattern = /^ *-?\d+(?:\.\d+)? *$/ const pattern = /^ *-?\d+(?:\.\d+)? *$/
if (parts.length != 2 if (parts.length != 2
|| !parts[0].match(pattern) || !parts[0].match(pattern)
|| !parts[1].match(pattern)) { || !parts[1].match(pattern)) {
oruga.notification.open({ this.$oruga.notification.open({
variant: 'warning', variant: 'warning',
message: "Coordinates are not valid.", message: "Coordinates are not valid.",
position: 'bottom', position: 'bottom',
}) })
coordinatesInput.value.focus() this.$refs.coordinates.focus()
return return
} }
coordinates.value = `${parts[0]},${parts[1]}` this.coordinates = `${parts[0]},${parts[1]}`
loading.value = true this.loading = true
const url = `https://api.weather.gov/points/${coordinates.value}` const url = `https://api.weather.gov/points/${this.coordinates}`
const response = await fetch(url) const response = await fetch(url)
const weather = await response.json() const weather = await response.json()
loading.value = false this.loading = false
if (weather.status == 404) { if (weather.status == 404) {
oruga.notification.open({ this.$oruga.notification.open({
variant: 'warning', variant: 'warning',
message: weather.title || "Data not found!", message: weather.title || "Data not found!",
position: 'bottom', position: 'bottom',
@ -84,24 +86,24 @@ async function loadCoordinates() {
const state = weather.properties.relativeLocation.properties.state const state = weather.properties.relativeLocation.properties.state
const cityState = `${city}, ${state}` const cityState = `${city}, ${state}`
locationStore.addLocation(coords, cityState) this.locationStore.addLocation(coords, cityState)
weatherStore.setCoordinates(coords) this.weatherStore.setCoordinates(coords)
weatherStore.setCityState(cityState) this.weatherStore.setCityState(cityState)
weatherStore.setWeather(weather) this.weatherStore.setWeather(weather)
// clear this so user sees empty input when they return // clear this so user sees empty input when they return
coordinates.value = null this.coordinates = null
router.push('/weather') this.$router.push('/weather')
},
showWeather(location) {
this.weatherStore.clearWeather()
this.weatherStore.setCoordinates(location.coordinates)
this.weatherStore.setCityState(location.cityState)
this.$router.push('/weather')
},
},
} }
function showWeather(location) {
weatherStore.clearWeather()
weatherStore.setCoordinates(location.coordinates)
weatherStore.setCityState(location.cityState)
router.push('/weather')
}
</script> </script>
<template> <template>
@ -110,7 +112,7 @@ function showWeather(location) {
<o-field grouped> <o-field grouped>
<o-input v-model="coordinates" <o-input v-model="coordinates"
ref="coordinatesInput" ref="coordinates"
placeholder="coordinates" placeholder="coordinates"
clearable /> clearable />
<o-button variant="primary" <o-button variant="primary"

View file

@ -1,37 +1,45 @@
<script setup> <script setup>
import { ref, onActivated } from 'vue' import { mapStores } from 'pinia'
import { useWeatherStore } from '../stores/weather' import { useWeatherStore } from '../stores/weather'
</script>
<script>
export default {
const weatherStore = useWeatherStore() data() {
return {
const coordinates = ref(null) coordinates: null,
const chunks = ref([]) hourly: null,
const refreshing = ref(false) chunks: [],
onActivated(() => {
if (coordinates.value != weatherStore.coordinates) {
refreshing.value = true
chunks.value = []
fetchHourly().then(() => {
refreshing.value = false
})
} }
}) },
async function fetchHourly() { computed: {
...mapStores(useWeatherStore),
},
const weather = await weatherStore.getWeather() activated() {
coordinates.value = weatherStore.coordinates if (this.coordinates != this.weatherStore.coordinates) {
this.hourly = null
this.chunks = []
this.fetchHourly()
}
},
methods: {
async fetchHourly() {
const weather = await this.weatherStore.getWeather()
this.coordinates = this.weatherStore.coordinates
const url = weather.properties.forecastHourly const url = weather.properties.forecastHourly
const response = await fetch(url) const response = await fetch(url)
const hourly = await response.json() this.hourly = await response.json()
chunks.value = [] this.chunks = []
let chunk = null let chunk = null
for (let period of hourly.properties.periods.slice(0, 12)) { for (let period of this.hourly.properties.periods.slice(0, 12)) {
const date = new Date(period.startTime).toLocaleDateString('en-US', { const date = new Date(period.startTime).toLocaleDateString('en-US', {
dateStyle: 'full', dateStyle: 'full',
}) })
@ -40,46 +48,29 @@ async function fetchHourly() {
date, date,
periods: [], periods: [],
} }
chunks.value.push(chunk) this.chunks.push(chunk)
} }
chunk.periods.push(period) chunk.periods.push(period)
} }
} },
renderTime(period) {
async function refreshHourly() {
refreshing.value = true
weatherStore.clearWeather(true)
chunks.value = []
await fetchHourly()
refreshing.value = false
}
function renderTime(period) {
const date = new Date(period.startTime) const date = new Date(period.startTime)
return date.toLocaleTimeString('en-US', {timeStyle: 'short'}) return date.toLocaleTimeString('en-US', {timeStyle: 'short'})
},
},
} }
</script> </script>
<template> <template>
<main> <main>
<div class="block"
style="display: flex; align-items: center; justify-content: space-between;">
<o-button variant="primary" <o-button variant="primary"
size="small"
icon-left="arrow-left" icon-left="arrow-left"
@click="$router.push('/weather')"> @click="$router.push('/weather')">
Back Back
</o-button> </o-button>
<o-button variant="primary"
icon-left="refresh"
@click="refreshHourly()"
:disabled="refreshing">
{{ refreshing ? "Refreshing" : "Refresh" }}
</o-button>
</div>
<h5 class="is-size-5">{{ weatherStore.cityState }}</h5> <h5 class="is-size-5">{{ weatherStore.cityState }}</h5>
<br /> <br />

View file

@ -1,99 +1,71 @@
<script setup> <script setup>
import { ref, computed, onActivated } from 'vue' import { mapStores } from 'pinia'
import { useRouter } from 'vue-router'
import { useWeatherStore } from '../stores/weather' import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location' import { useLocationStore } from '../stores/location'
</script>
<script>
export default {
const router = useRouter() data() {
const locationStore = useLocationStore() return {
const weatherStore = useWeatherStore() coordinates: null,
}
},
const coordinates = ref(null) computed: {
const refreshing = ref(false) ...mapStores(useWeatherStore, useLocationStore),
const timestamp = ref(new Date().getTime())
panelHeadingTitle() {
const panelHeadingTitle = computed(() => { if (!this.weatherStore.forecast) {
if (!weatherStore.forecast) {
return "Forecast" return "Forecast"
} }
let generated = new Date(weatherStore.forecast.properties.generatedAt) let generated = new Date(this.weatherStore.forecast.properties.generatedAt)
generated = generated.toLocaleTimeString('en-US', {timeStyle: 'short'}) generated = generated.toLocaleTimeString('en-US', {timeStyle: 'short'})
return `Forecast @ ${generated}` return `Forecast @ ${generated}`
}) },
},
activated() {
this.fetchWeather()
},
const radarLatestURL = computed(() => { methods: {
if (weatherStore.weather) {
return `${weatherStore.radarLatestURL}?t=${timestamp.value}`
}
})
coordinatesUpdated(coordinates) {
this.weatherStore.clearWeather()
this.weatherStore.setCoordinates(coordinates)
this.fetchWeather()
},
const radarLoopURL = computed(() => { async fetchWeather() {
if (weatherStore.weather) {
return `${weatherStore.radarLoopURL}?t=${timestamp.value}`
}
})
if (!this.weatherStore.coordinates) {
onActivated(() => { this.$router.push('/')
if (!weatherStore.coordinates) {
router.push('/')
return return
} }
refreshing.value = true const forecast = await this.weatherStore.getForecast()
fetchWeather().then(() => { this.coordinates = this.weatherStore.coordinates
refreshing.value = false },
})
})
getShortForecast(period) {
async function coordinatesUpdated(coords) {
refreshing.value = true
weatherStore.clearWeather()
weatherStore.setCoordinates(coords)
await fetchWeather()
refreshing.value = false
}
async function fetchWeather() {
await weatherStore.getForecast()
coordinates.value = weatherStore.coordinates
}
async function refreshWeather() {
refreshing.value = true
weatherStore.clearWeather(true)
timestamp.value = new Date().getTime()
await fetchWeather()
refreshing.value = false
}
function getShortForecast(period) {
return period.shortForecast.replace(/Showers And Thunderstorms/g, 'T-storms') return period.shortForecast.replace(/Showers And Thunderstorms/g, 'T-storms')
},
showHourly(period) {
this.$router.push('/hourly')
},
},
} }
function showHourly(period) {
router.push('/hourly')
}
</script> </script>
<template> <template>
<main> <main>
<div style="display: flex; flex-direction: column;"> <div style="display: flex; flex-direction: column;">
<div class="block" <o-field>
style="display: flex; justify-content: space-between; align-items: center;">
<o-select v-if="weatherStore.weather" <o-select v-if="weatherStore.weather"
v-model="coordinates" v-model="coordinates"
@update:model-value="coordinatesUpdated"> @update:model-value="coordinatesUpdated">
@ -103,24 +75,7 @@ function showHourly(period) {
{{ location.cityState }} {{ location.cityState }}
</option> </option>
</o-select> </o-select>
<o-button variant="primary" </o-field>
icon-left="refresh"
@click="refreshWeather()"
:disabled="refreshing">
{{ refreshing ? "Refreshing" : "Refresh" }}
</o-button>
</div>
<div v-if="weatherStore.alerts?.features?.length"
class="block">
<o-notification variant="warning"
icon="warning"
@click="$router.push('/alerts')"
style="cursor: pointer;">
<p class="has-text-weight-bold">Watches In Effect</p>
<p>Updated {{ new Date(weatherStore.alerts.updated).toLocaleTimeString('en-US', {timeStyle: 'short'}) }}</p>
</o-notification>
</div>
<nav class="panel weather-panel"> <nav class="panel weather-panel">
@ -137,7 +92,6 @@ function showHourly(period) {
:key="period.number" :key="period.number"
class="weather-period is-size-7 has-text-weight-bold" class="weather-period is-size-7 has-text-weight-bold"
:class="{daytime: period.isDaytime}" :class="{daytime: period.isDaytime}"
style="cursor: pointer;"
@click="showHourly(period)"> @click="showHourly(period)">
<p>{{ period.name }}</p> <p>{{ period.name }}</p>
@ -166,23 +120,16 @@ function showHourly(period) {
</nav> </nav>
<nav class="panel weather-panel"> <nav class="panel weather-panel">
<div class="panel-heading" <p class="panel-heading">
style="display: flex; align-items: center; justify-content: space-between;"> Radar
<p>Radar</p> </p>
<o-button tag="a"
href="https://radar.weather.gov/ridge/standard/CONUS-LARGE_loop.gif"
target="_blank"
icon-left="external-link">
National Radar
</o-button>
</div>
<div class="panel-block"> <div class="panel-block">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<img :src="radarLatestURL" /> <img :src="weatherStore.radarLatestURL" />
</div> </div>
<div class="column"> <div class="column">
<img :src="radarLoopURL" /> <img :src="weatherStore.radarLoopURL" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -24,7 +24,7 @@ def release(c):
version = js['version'] version = js['version']
# build the app, create zip archive # build the app, create zip archive
c.run("bash -lc 'nvm use lts/iron; npm run build'") c.run('npm run build')
os.chdir('dist') os.chdir('dist')
filename = f'myweather-{version}.zip' filename = f'myweather-{version}.zip'
c.run(f'zip --recurse-paths {filename} *') c.run(f'zip --recurse-paths {filename} *')