Basic app functions are working

definitely on the "minimal" side but this *mostly* covers my own usage
of the old mobile.weather.gov app
This commit is contained in:
Lance Edgar 2024-06-08 13:15:29 -05:00
parent 397b7b02e5
commit 5a584982e4
28 changed files with 2020 additions and 473 deletions

View file

@ -0,0 +1,62 @@
<script setup>
import { mapStores } from 'pinia'
import { useLocationStore } from '../stores/location'
</script>
<script>
export default {
computed: {
...mapStores(useLocationStore),
},
methods: {
deleteLocation(location) {
this.locationStore.removeLocation(location)
},
},
}
</script>
<template>
<main>
<o-button variant="primary"
size="small"
icon-left="arrow-left"
@click="$router.push('/')">
Back
</o-button>
<h5 class="is-size-5">Saved Locations</h5>
<br />
<div v-for="location in locationStore.locations"
:key="location.coordinates"
class="block location">
<div>
<p>{{ location.cityState }}</p>
<p class="is-size-7">{{ location.coordinates }}</p>
</div>
<o-button variant="danger"
size="small"
@click="deleteLocation(location)">
<o-icon icon="trash" />
</o-button>
</div>
</main>
</template>
<style scoped>
div.location {
border: 1px solid black;
border-radius: 5px;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View file

@ -1,9 +1,134 @@
<script setup>
import TheWelcome from '../components/TheWelcome.vue'
import { mapStores } from 'pinia'
import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location'
</script>
<script>
export default {
data() {
return {
coordinates: null,
loading: false,
}
},
computed: {
...mapStores(useWeatherStore, useLocationStore),
},
methods: {
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')
},
},
}
</script>
<template>
<main>
<TheWelcome />
<br />
<o-field grouped>
<o-input v-model="coordinates"
ref="coordinates"
placeholder="coordinates"
clearable />
<o-button variant="primary"
icon-left="arrow-right"
@click="loadCoordinates()"
:disabled="loading">
Go!
</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 v-if="locationStore.locations.length"
class="location">
<o-button icon-right="edit"
expanded
@click="editLocations()">
Edit List
</o-button>
</div>
</main>
</template>
<style scoped>
div.location {
padding: 0.25rem;
}
</style>

117
src/views/HourlyView.vue Normal file
View file

@ -0,0 +1,117 @@
<script setup>
import { mapStores } from 'pinia'
import { useWeatherStore } from '../stores/weather'
</script>
<script>
export default {
data() {
return {
coordinates: null,
hourly: null,
chunks: [],
}
},
computed: {
...mapStores(useWeatherStore),
},
activated() {
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 response = await fetch(url)
this.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)
}
},
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>
<h5 class="is-size-5">{{ weatherStore.cityState }}</h5>
<br />
<div v-for="chunk in chunks"
:key="chunk.date">
<p class="block has-text-weight-bold">{{ chunk.date }}</p>
<div v-for="period in chunk.periods"
:key="period.number"
class="hourly-period is-size-7 has-text-weight-bold">
<div>
<p>{{ renderTime(period) }}</p>
<img :src="period.icon" />
</div>
<div>
<p>{{ period.shortForecast }}</p>
<p>Temp: {{ period.temperature }} &deg; {{ period.temperatureUnit }}</p>
<p>Wind: {{ period.windDirection }} @ {{ period.windSpeed }}</p>
</div>
</div>
<br />
</div>
</main>
</template>
<style>
.hourly-period {
background: #eee;
border: 1px solid #b3b3b3;
border-collapse: collapse;
padding: 0.5rem 1rem;
display: flex;
gap: 1rem;
align-items: center;
}
</style>

139
src/views/WeatherView.vue Normal file
View file

@ -0,0 +1,139 @@
<script setup>
import { mapStores } from 'pinia'
import { useWeatherStore } from '../stores/weather'
import { useLocationStore } from '../stores/location'
</script>
<script>
export default {
data() {
return {
coordinates: null,
}
},
computed: {
...mapStores(useWeatherStore, useLocationStore),
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}`
},
},
activated() {
this.fetchWeather()
},
methods: {
coordinatesUpdated(coordinates) {
this.weatherStore.clearWeather()
this.weatherStore.setCoordinates(coordinates)
this.fetchWeather()
},
async fetchWeather() {
if (!this.weatherStore.coordinates) {
this.$router.push('/')
return
}
const forecast = await this.weatherStore.getForecast()
this.coordinates = this.weatherStore.coordinates
},
showHourly(period) {
this.$router.push('/hourly')
},
},
}
</script>
<template>
<main>
<div style="display: flex; flex-direction: column;">
<o-field>
<o-select v-if="weatherStore.weather"
v-model="coordinates"
@update:model-value="coordinatesUpdated">
<option v-for="location in locationStore.locations"
:key="location.coordinates"
:value="location.coordinates">
{{ location.cityState }}
</option>
</o-select>
</o-field>
<nav class="panel weather-panel">
<p class="panel-heading">
{{ panelHeadingTitle }}
</p>
<div class="panel-block">
<div v-if="weatherStore.forecast"
style="display: flex;">
<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}"
@click="showHourly(period)">
<p>{{ period.name }}</p>
<div>
<img :src="period.icon" />
</div>
<p>{{ period.shortForecast }}</p>
<p>
<span v-if="period.isDaytime">Hi</span>
<span v-else>Lo</span>
{{ period.temperature }} &deg; {{ period.temperatureUnit }}
</p>
</div>
</div>
<div v-if="!weatherStore.forecast">
fetching weather data...
</div>
</div>
</nav>
</div>
</main>
</template>
<style scoped>
.weather-period {
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
background-color: gray;
color: white;
padding: 0.5rem;
}
.weather-period.daytime {
background-color: white;
color: black;
}
</style>