Compare commits

..

23 commits

Author SHA1 Message Date
cfe916ec72 bump to version 0.1.11 2025-10-06 11:08:52 -05:00
01b9644243 fix: update source code info for about page 2025-10-06 11:06:53 -05:00
Lance Edgar
52b83d05bb build: explicitly use npm lts/iron when building package 2024-07-02 19:49:25 -05:00
Lance Edgar
a09d50af2c Update changelog 2024-06-09 19:32:46 -05:00
Lance Edgar
c92d9aae0d Fix URL bug when refreshing weather radar 2024-06-09 19:32:17 -05:00
Lance Edgar
d4cf4e1c3e Update changelog 2024-06-09 19:28:02 -05:00
Lance Edgar
fa1e702173 Add timestamp param to bust cache when refreshing radar images 2024-06-09 19:26:42 -05:00
Lance Edgar
cd615cb020 Warning notification should not butt up against forecast panel 2024-06-09 16:41:27 -05:00
Lance Edgar
d2afc8469d Update changelog 2024-06-09 16:29:15 -05:00
Lance Edgar
a1f84465cc Add refresh buttons to weather data pages 2024-06-09 16:27:04 -05:00
Lance Edgar
7a14101e01 Convert all view components to use Composition API
might as well start getting used to this new hotness
2024-06-09 15:45:38 -05:00
Lance Edgar
5e238aa2aa Add link to national radar map (live image) 2024-06-09 00:32:57 -05:00
Lance Edgar
e611803bd7 Update changelog 2024-06-08 23:17:51 -05:00
Lance Edgar
0710f2238c Add basic support for active weather alerts
yikes nobody should stake their life on how well this works, but it
does seem to basically work..
2024-06-08 23:16:29 -05:00
Lance Edgar
7ed314b5bf Update changelog 2024-06-08 21:31:18 -05:00
Lance Edgar
e446084d62 Add proper About page 2024-06-08 21:30:24 -05:00
Lance Edgar
8109c6d3f6 Update changelog 2024-06-08 20:35:33 -05:00
Lance Edgar
aa12f028a5 Tighten up the forecast period panels a bit 2024-06-08 20:34:47 -05:00
Lance Edgar
757e5ce6c8 Update changelog 2024-06-08 19:58:59 -05:00
Lance Edgar
f82540c677 Fix horizontal scroll for main forecast panel 2024-06-08 19:58:31 -05:00
Lance Edgar
fe411cd27c Update changelog 2024-06-08 19:37:04 -05:00
Lance Edgar
b983a34bc9 Make buttons half-wide on home screen (desktop mode)
having a button take up full screen width is a bit much
2024-06-08 19:35:45 -05:00
Lance Edgar
5d079e1208 Tell user to refresh the page, if access to location has failed
at least in my local firefox on laptop, it would not prompt user again
until page was refreshed
2024-06-08 19:35:03 -05:00
16 changed files with 591 additions and 272 deletions

1
.gitignore vendored
View file

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

View file

@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 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
### Changed
- Tighten up the forecast period panels a bit.
## 0.1.4 - 2024-06-08
### Changed
- Fix horizontal scroll for main forecast panel.
## 0.1.3 - 2024-06-08
### Changed
- Tell user to refresh the page, if access to location has failed.
- Make buttons half-wide on home screen (desktop mode).
## 0.1.2 - 2024-06-08
### Added
- Add "use my current location" button to home page.

4
package-lock.json generated
View file

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

View file

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

View file

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

10
src/appsettings.js Normal file
View file

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

View file

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

View file

@ -10,6 +10,7 @@ const getDefaults = () => {
coordinates,
cityState,
weather: null,
alerts: null,
forecast: null,
radarLatestURL: null,
radarLoopURL: null,
@ -25,21 +26,26 @@ export const useWeatherStore = defineStore('weather', {
actions: {
clearWeather() {
this.setCoordinates(null)
this.setCityState(null)
clearWeather(keepCoordinates) {
if (!keepCoordinates) {
this.setCoordinates(null)
this.setCityState(null)
}
this.setWeather(null)
this.setForecast(null)
this.radarLatestURL = null
this.radarLoopURL = null
this.alerts = null
},
async getWeather() {
if (!this.weather) {
let url
let response
const url = `https://api.weather.gov/points/${this.coordinates}`
const response = await fetch(url)
url = `https://api.weather.gov/points/${this.coordinates}`
response = await fetch(url)
const weather = await response.json()
if (weather.status == 404) {
throw new Error(`Data not found for ${this.coordinates}`)
@ -57,6 +63,65 @@ export const useWeatherStore = defineStore('weather', {
this.radarLoopURL = `https://radar.weather.gov/ridge/standard/${station}_loop.gif`
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
@ -66,7 +131,7 @@ export const useWeatherStore = defineStore('weather', {
if (!this.forecast) {
const weather = await this.getWeather(this.coordinates)
const weather = await this.getWeather()
const url = weather.properties.forecast
const response = await fetch(url)

View file

@ -1,15 +1,57 @@
<script setup>
import appsettings from '../appsettings'
</script>
<template>
<div class="about">
<h1>This is an about page</h1>
<h4 class="is-size-4">{{ appsettings.appTitle }} {{ appsettings.appVersion }}</h4>
<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>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

65
src/views/AlertsView.vue Normal file
View file

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

View file

@ -1,102 +1,107 @@
<script setup>
import { mapStores } from 'pinia'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location'
</script>
import { useOruga } from '@oruga-ui/oruga-next'
<script>
export default {
data() {
return {
coordinates: null,
loading: false,
const router = useRouter()
const locationStore = useLocationStore()
const weatherStore = useWeatherStore()
const oruga = useOruga()
const coordinates = ref(null)
const coordinatesInput = ref()
const loading = ref(false)
const locationAccessBlocked = ref(false)
function useCurrentLocation() {
if (locationAccessBlocked.value) {
alert("You must refresh the page first, then try again.")
return
}
navigator.geolocation.getCurrentPosition(loc => {
coordinates.value = `${loc.coords.latitude},${loc.coords.longitude}`
loadCoordinates()
}, error => {
if (error.code == 1) { // PERMISSION_DENIED
locationAccessBlocked.value = true
}
},
computed: {
...mapStores(useWeatherStore, useLocationStore),
},
methods: {
useCurrentLocation() {
if (location.protocol == 'http:') {
alert("Sorry, this only works for secure sites.")
}
navigator.geolocation.getCurrentPosition(loc => {
this.coordinates = `${loc.coords.latitude},${loc.coords.longitude}`
this.loadCoordinates()
})
},
editLocations() {
this.$router.push('/edit-list')
},
async loadCoordinates() {
if (!this.coordinates) {
this.$refs.coordinates.focus()
return
}
const parts = this.coordinates.split(/(?: +| *\, *)/)
const pattern = /^ *-?\d+(?:\.\d+)? *$/
if (parts.length != 2
|| !parts[0].match(pattern)
|| !parts[1].match(pattern)) {
this.$oruga.notification.open({
variant: 'warning',
message: "Coordinates are not valid.",
position: 'bottom',
})
this.$refs.coordinates.focus()
return
}
this.coordinates = `${parts[0]},${parts[1]}`
this.loading = true
const url = `https://api.weather.gov/points/${this.coordinates}`
const response = await fetch(url)
const weather = await response.json()
this.loading = false
if (weather.status == 404) {
this.$oruga.notification.open({
variant: 'warning',
message: weather.title || "Data not found!",
position: 'bottom',
})
return
}
let coords = weather.geometry.coordinates
coords = `${coords[1].toFixed(4)},${coords[0].toFixed(4)}`
const city = weather.properties.relativeLocation.properties.city
const state = weather.properties.relativeLocation.properties.state
const cityState = `${city}, ${state}`
this.locationStore.addLocation(coords, cityState)
this.weatherStore.setCoordinates(coords)
this.weatherStore.setCityState(cityState)
this.weatherStore.setWeather(weather)
// clear this so user sees empty input when they return
this.coordinates = null
this.$router.push('/weather')
},
showWeather(location) {
this.weatherStore.clearWeather()
this.weatherStore.setCoordinates(location.coordinates)
this.weatherStore.setCityState(location.cityState)
this.$router.push('/weather')
},
},
alert(`error.code = ${error.code}\n\n${error.message}`)
})
}
function editLocations() {
router.push('/edit-list')
}
async function loadCoordinates() {
if (!coordinates.value) {
coordinatesInput.value.focus()
return
}
const parts = coordinates.value.split(/(?: +| *\, *)/)
const pattern = /^ *-?\d+(?:\.\d+)? *$/
if (parts.length != 2
|| !parts[0].match(pattern)
|| !parts[1].match(pattern)) {
oruga.notification.open({
variant: 'warning',
message: "Coordinates are not valid.",
position: 'bottom',
})
coordinatesInput.value.focus()
return
}
coordinates.value = `${parts[0]},${parts[1]}`
loading.value = true
const url = `https://api.weather.gov/points/${coordinates.value}`
const response = await fetch(url)
const weather = await response.json()
loading.value = false
if (weather.status == 404) {
oruga.notification.open({
variant: 'warning',
message: weather.title || "Data not found!",
position: 'bottom',
})
return
}
let coords = weather.geometry.coordinates
coords = `${coords[1].toFixed(4)},${coords[0].toFixed(4)}`
const city = weather.properties.relativeLocation.properties.city
const state = weather.properties.relativeLocation.properties.state
const cityState = `${city}, ${state}`
locationStore.addLocation(coords, cityState)
weatherStore.setCoordinates(coords)
weatherStore.setCityState(cityState)
weatherStore.setWeather(weather)
// clear this so user sees empty input when they return
coordinates.value = null
router.push('/weather')
}
function showWeather(location) {
weatherStore.clearWeather()
weatherStore.setCoordinates(location.coordinates)
weatherStore.setCityState(location.cityState)
router.push('/weather')
}
</script>
<template>
@ -105,7 +110,7 @@ export default {
<o-field grouped>
<o-input v-model="coordinates"
ref="coordinates"
ref="coordinatesInput"
placeholder="coordinates"
clearable />
<o-button variant="primary"
@ -116,34 +121,40 @@ export default {
</o-button>
</o-field>
<div v-for="location in locationStore.locations"
:key="location.coordinates"
class="location">
<o-button variant="primary"
icon-right="arrow-right"
expanded
@click="showWeather(location)">
{{ location.cityState }}
</o-button>
</div>
<div class="columns">
<div class="column is-half">
<div v-if="locationStore.locations.length"
class="location">
<o-button icon-right="edit"
expanded
@click="editLocations()">
Edit List
</o-button>
</div>
<div v-for="location in locationStore.locations"
:key="location.coordinates"
class="location">
<o-button variant="primary"
icon-right="arrow-right"
expanded
@click="showWeather(location)">
{{ location.cityState }}
</o-button>
</div>
<br />
<div class="location">
<o-button variant="primary"
icon-right="location-dot"
expanded
@click="useCurrentLocation()">
Use My Current Location
</o-button>
<div v-if="locationStore.locations.length"
class="location">
<o-button icon-right="edit"
expanded
@click="editLocations()">
Edit List
</o-button>
</div>
<br />
<div class="location">
<o-button variant="primary"
icon-right="location-dot"
expanded
@click="useCurrentLocation()">
Use My Current Location
</o-button>
</div>
</div>
</div>
</main>

View file

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

View file

@ -1,67 +1,99 @@
<script setup>
import { mapStores } from 'pinia'
import { ref, computed, onActivated } from 'vue'
import { useRouter } from 'vue-router'
import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location'
</script>
<script>
export default {
data() {
return {
coordinates: null,
}
},
const router = useRouter()
const locationStore = useLocationStore()
const weatherStore = useWeatherStore()
computed: {
...mapStores(useWeatherStore, useLocationStore),
const coordinates = ref(null)
const refreshing = ref(false)
const timestamp = ref(new Date().getTime())
panelHeadingTitle() {
if (!this.weatherStore.forecast) {
return "Forecast"
}
let generated = new Date(this.weatherStore.forecast.properties.generatedAt)
generated = generated.toLocaleTimeString('en-US', {timeStyle: 'short'})
return `Forecast @ ${generated}`
},
},
const panelHeadingTitle = computed(() => {
if (!weatherStore.forecast) {
return "Forecast"
}
activated() {
this.fetchWeather()
},
let generated = new Date(weatherStore.forecast.properties.generatedAt)
generated = generated.toLocaleTimeString('en-US', {timeStyle: 'short'})
return `Forecast @ ${generated}`
})
methods: {
coordinatesUpdated(coordinates) {
this.weatherStore.clearWeather()
this.weatherStore.setCoordinates(coordinates)
this.fetchWeather()
},
const radarLatestURL = computed(() => {
if (weatherStore.weather) {
return `${weatherStore.radarLatestURL}?t=${timestamp.value}`
}
})
async fetchWeather() {
if (!this.weatherStore.coordinates) {
this.$router.push('/')
return
}
const radarLoopURL = computed(() => {
if (weatherStore.weather) {
return `${weatherStore.radarLoopURL}?t=${timestamp.value}`
}
})
const forecast = await this.weatherStore.getForecast()
this.coordinates = this.weatherStore.coordinates
},
showHourly(period) {
this.$router.push('/hourly')
},
},
onActivated(() => {
if (!weatherStore.coordinates) {
router.push('/')
return
}
refreshing.value = true
fetchWeather().then(() => {
refreshing.value = false
})
})
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')
}
function showHourly(period) {
router.push('/hourly')
}
</script>
<template>
<main>
<div style="display: flex; flex-direction: column;">
<o-field>
<div class="block"
style="display: flex; justify-content: space-between; align-items: center;">
<o-select v-if="weatherStore.weather"
v-model="coordinates"
@update:model-value="coordinatesUpdated">
@ -71,7 +103,24 @@ export default {
{{ location.cityState }}
</option>
</o-select>
</o-field>
<o-button variant="primary"
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">
@ -82,12 +131,13 @@ export default {
<div class="panel-block">
<div v-if="weatherStore.forecast"
style="display: flex;">
style="display: flex; overflow-x: scroll;">
<div v-for="period in weatherStore.forecast.properties.periods"
:key="period.number"
class="weather-period is-size-7 has-text-weight-bold"
:class="{daytime: period.isDaytime}"
style="cursor: pointer;"
@click="showHourly(period)">
<p>{{ period.name }}</p>
@ -96,7 +146,7 @@ export default {
<img :src="period.icon" />
</div>
<p>{{ period.shortForecast }}</p>
<p>{{ getShortForecast(period) }}</p>
<p>
<span v-if="period.isDaytime">Hi</span>
@ -116,16 +166,23 @@ export default {
</nav>
<nav class="panel weather-panel">
<p class="panel-heading">
Radar
</p>
<div class="panel-heading"
style="display: flex; align-items: center; justify-content: space-between;">
<p>Radar</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="columns">
<div class="column">
<img :src="weatherStore.radarLatestURL" />
<img :src="radarLatestURL" />
</div>
<div class="column">
<img :src="weatherStore.radarLoopURL" />
<img :src="radarLoopURL" />
</div>
</div>
</div>
@ -139,6 +196,7 @@ export default {
.weather-period {
width: 100px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;

View file

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