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:
parent
397b7b02e5
commit
5a584982e4
28 changed files with 2020 additions and 473 deletions
62
src/views/EditListView.vue
Normal file
62
src/views/EditListView.vue
Normal 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>
|
|
@ -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
117
src/views/HourlyView.vue
Normal 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 }} ° {{ 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
139
src/views/WeatherView.vue
Normal 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 }} ° {{ 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>
|
Loading…
Add table
Add a link
Reference in a new issue