124 lines
3.7 KiB
Vue
124 lines
3.7 KiB
Vue
<template>
|
|
<IonCard>
|
|
<IonCardHeader>
|
|
<IonCardTitle>Weather</IonCardTitle>
|
|
<IonCardSubtitle>Marina conditions</IonCardSubtitle>
|
|
</IonCardHeader>
|
|
<IonCardContent>
|
|
<div v-if="loading" class="ion-text-center ion-padding">
|
|
<IonSpinner name="crescent" />
|
|
</div>
|
|
<p v-else-if="error" class="empty-text">{{ error }}</p>
|
|
<div v-else class="weather-grid">
|
|
<div class="weather-item">
|
|
<IonIcon :icon="thermometerOutline" />
|
|
<span class="value">{{ weather.temp }}°C</span>
|
|
<span class="label">Temperature</span>
|
|
</div>
|
|
<div class="weather-item">
|
|
<IonIcon :icon="partlySunnyOutline" />
|
|
<span class="value">{{ weather.description }}</span>
|
|
<span class="label">Conditions</span>
|
|
</div>
|
|
<div class="weather-item">
|
|
<IonIcon
|
|
:icon="arrowUpOutline"
|
|
:style="{ transform: `rotate(${weather.windDir}deg)` }"
|
|
/>
|
|
<span class="value">{{ weather.windSpeed }} kn</span>
|
|
<span class="label">Wind</span>
|
|
</div>
|
|
</div>
|
|
</IonCardContent>
|
|
</IonCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle,
|
|
IonCardContent, IonSpinner, IonIcon,
|
|
} from '@ionic/vue'
|
|
import { thermometerOutline, partlySunnyOutline, arrowUpOutline } from 'ionicons/icons'
|
|
|
|
const LAT = 43.4412629
|
|
const LON = -79.6696725
|
|
|
|
const WMO: Record<number, string> = {
|
|
0: 'Clear', 1: 'Mostly clear', 2: 'Partly cloudy', 3: 'Overcast',
|
|
45: 'Fog', 48: 'Icy fog',
|
|
51: 'Light drizzle', 53: 'Drizzle', 55: 'Heavy drizzle',
|
|
61: 'Light rain', 63: 'Rain', 65: 'Heavy rain',
|
|
71: 'Light snow', 73: 'Snow', 75: 'Heavy snow',
|
|
80: 'Showers', 81: 'Showers', 82: 'Heavy showers',
|
|
95: 'Thunderstorm', 96: 'T-storm + hail', 99: 'Severe t-storm',
|
|
}
|
|
|
|
const CACHE_KEY = 'weather_cache'
|
|
const CACHE_TTL_MS = 10 * 60 * 1000 // 10 minutes
|
|
|
|
interface WeatherData { temp: number; windSpeed: number; windDir: number; description: string }
|
|
interface WeatherCache { data: WeatherData; fetchedAt: number }
|
|
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const weather = ref<WeatherData>({ temp: 0, windSpeed: 0, windDir: 0, description: '' })
|
|
|
|
onMounted(async () => {
|
|
const cached = localStorage.getItem(CACHE_KEY)
|
|
if (cached) {
|
|
const { data, fetchedAt }: WeatherCache = JSON.parse(cached)
|
|
if (Date.now() - fetchedAt < CACHE_TTL_MS) {
|
|
weather.value = data
|
|
loading.value = false
|
|
return
|
|
}
|
|
}
|
|
|
|
try {
|
|
const url =
|
|
`https://api.open-meteo.com/v1/forecast` +
|
|
`?latitude=${LAT}&longitude=${LON}` +
|
|
`¤t=temperature_2m,wind_speed_10m,wind_direction_10m,weather_code` +
|
|
`&wind_speed_unit=kn`
|
|
const res = await fetch(url)
|
|
if (!res.ok) throw new Error()
|
|
const json = await res.json()
|
|
const c = json.current
|
|
const data: WeatherData = {
|
|
temp: Math.round(c.temperature_2m),
|
|
windSpeed: Math.round(c.wind_speed_10m),
|
|
windDir: c.wind_direction_10m,
|
|
description: WMO[c.weather_code as number] ?? 'Unknown',
|
|
}
|
|
weather.value = data
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify({ data, fetchedAt: Date.now() }))
|
|
} catch {
|
|
error.value = 'Weather data unavailable'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.weather-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
.weather-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
.weather-item ion-icon {
|
|
font-size: 1.75rem;
|
|
color: var(--ion-color-primary);
|
|
}
|
|
.value { font-size: 1rem; font-weight: 600; }
|
|
.label { font-size: 0.75rem; color: var(--ion-color-medium); }
|
|
.empty-text { color: var(--ion-color-medium); margin: 0; }
|
|
</style>
|