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