Compare commits
10 commits
e611803bd7
...
52b83d05bb
Author | SHA1 | Date | |
---|---|---|---|
![]() |
52b83d05bb | ||
![]() |
a09d50af2c | ||
![]() |
c92d9aae0d | ||
![]() |
d4cf4e1c3e | ||
![]() |
fa1e702173 | ||
![]() |
cd615cb020 | ||
![]() |
d2afc8469d | ||
![]() |
a1f84465cc | ||
![]() |
7a14101e01 | ||
![]() |
5e238aa2aa |
12 changed files with 321 additions and 261 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,3 +28,4 @@ coverage
|
|||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
*~
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 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.
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "myweather",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "myweather",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.10",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "myweather",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
@ -26,9 +26,11 @@ 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
|
||||
|
@ -103,10 +105,10 @@ export const useWeatherStore = defineStore('weather', {
|
|||
// put "likely" before "possible" alerts
|
||||
newAlerts.features.sort((a, b) => {
|
||||
|
||||
if (a.properties.certainty == 'Likely' && b.properties.certainty == 'Possible') {
|
||||
if (a.properties.certainty == 'Likely' && b.properties.certainty != 'Likely') {
|
||||
return -1
|
||||
}
|
||||
if (a.properties.certainty == 'Possible' && b.properties.certainty == 'Likely') {
|
||||
if (a.properties.certainty != 'Likely' && b.properties.certainty == 'Likely') {
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -129,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)
|
||||
|
|
|
@ -2,17 +2,6 @@
|
|||
import appsettings from '../appsettings'
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
appsettings,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about">
|
||||
<h4 class="is-size-4">{{ appsettings.appTitle }} {{ appsettings.appVersion }}</h4>
|
||||
|
|
|
@ -1,30 +1,44 @@
|
|||
<script setup>
|
||||
import { mapStores } from 'pinia'
|
||||
import { ref, onActivated } from 'vue'
|
||||
import { useWeatherStore } from '../stores/weather'
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
const weatherStore = useWeatherStore()
|
||||
|
||||
computed: {
|
||||
...mapStores(useWeatherStore),
|
||||
},
|
||||
const refreshing = ref(false)
|
||||
|
||||
activated() {
|
||||
this.weatherStore.getWeather()
|
||||
},
|
||||
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>
|
||||
|
||||
<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="refreshAlerts()"
|
||||
:disabled="refreshing">
|
||||
{{ refreshing ? "Refreshing" : "Refresh" }}
|
||||
</o-button>
|
||||
</div>
|
||||
|
||||
<h5 class="is-size-5">{{ weatherStore.cityState }}</h5>
|
||||
<h5 class="is-size-5">Alerts</h5>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,109 +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,
|
||||
locationAccessBlocked: 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 (this.locationAccessBlocked) {
|
||||
alert("You must refresh the page first, then try again.")
|
||||
return
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(loc => {
|
||||
this.coordinates = `${loc.coords.latitude},${loc.coords.longitude}`
|
||||
this.loadCoordinates()
|
||||
}, error => {
|
||||
if (error.code == 1) { // PERMISSION_DENIED
|
||||
this.locationAccessBlocked = true
|
||||
}
|
||||
alert(`error.code = ${error.code}\n\n${error.message}`)
|
||||
})
|
||||
},
|
||||
|
||||
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>
|
||||
|
@ -112,7 +110,7 @@ export default {
|
|||
|
||||
<o-field grouped>
|
||||
<o-input v-model="coordinates"
|
||||
ref="coordinates"
|
||||
ref="coordinatesInput"
|
||||
placeholder="coordinates"
|
||||
clearable />
|
||||
<o-button variant="primary"
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -1,73 +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,
|
||||
alerts: 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
|
||||
this.alerts = this.weatherStore.alerts
|
||||
},
|
||||
|
||||
getShortForecast(period) {
|
||||
return period.shortForecast.replace(/Showers And Thunderstorms/g, 'T-storms')
|
||||
},
|
||||
onActivated(() => {
|
||||
|
||||
showHourly(period) {
|
||||
this.$router.push('/hourly')
|
||||
},
|
||||
},
|
||||
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">
|
||||
|
@ -77,15 +103,22 @@ 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="alerts?.features?.length">
|
||||
<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(alerts.updated).toLocaleTimeString('en-US', {timeStyle: 'short'}) }}</p>
|
||||
<p>Updated {{ new Date(weatherStore.alerts.updated).toLocaleTimeString('en-US', {timeStyle: 'short'}) }}</p>
|
||||
</o-notification>
|
||||
</div>
|
||||
|
||||
|
@ -104,6 +137,7 @@ export default {
|
|||
: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>
|
||||
|
@ -132,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>
|
||||
|
|
2
tasks.py
2
tasks.py
|
@ -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} *')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue