refactor part 1
This commit is contained in:
49
client/package-lock.json
generated
49
client/package-lock.json
generated
@@ -8,6 +8,8 @@
|
||||
"name": "weather-station",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@ztimson/css-utils": "^1.3.5",
|
||||
"@ztimson/utils": "^0.29.5",
|
||||
"vue": "^3.5.38"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -22,6 +24,7 @@
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-vue-devtools": "^8.1.2",
|
||||
"vue": "^3.5.38",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-tsc": "^3.3.5"
|
||||
},
|
||||
@@ -269,6 +272,7 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -278,6 +282,7 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -311,6 +316,7 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
@@ -475,6 +481,7 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
@@ -561,6 +568,7 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -1372,6 +1380,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
|
||||
"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
@@ -1385,6 +1394,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
|
||||
"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.38",
|
||||
@@ -1395,6 +1405,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
|
||||
"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
@@ -1412,6 +1423,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
|
||||
"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.38",
|
||||
@@ -1472,6 +1484,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
|
||||
"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.38"
|
||||
@@ -1481,6 +1494,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
|
||||
"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.38",
|
||||
@@ -1491,6 +1505,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
|
||||
"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.38",
|
||||
@@ -1503,6 +1518,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
|
||||
"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.38",
|
||||
@@ -1516,6 +1532,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
|
||||
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
@@ -1548,6 +1565,21 @@
|
||||
"unzipit": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ztimson/css-utils": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/css-utils/-/css-utils-1.3.5.tgz",
|
||||
"integrity": "sha512-evQgelNCkol8CaD1ppWYQOWmGoku5pVz1LTx9WH0p/JtKSDjd1lEubKDsczh4qVlRJsKvyRD8f9TJcD98DuMYA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ztimson/utils": {
|
||||
"version": "0.29.5",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
|
||||
"integrity": "sha512-8mUuhi//3agwrueR006emOvJu1JXxVEryJmD3nkEmK4yQ9qS24oZijdoaiLRid/6xc75j3Fk6YjmnYmjq61iPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"var-persist": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
||||
@@ -1758,6 +1790,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -1849,6 +1882,7 @@
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -1881,6 +1915,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
@@ -2420,6 +2455,7 @@
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -2462,6 +2498,7 @@
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2664,6 +2701,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -2696,6 +2734,7 @@
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3316,6 +3355,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3398,7 +3438,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -3473,6 +3513,12 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/var-persist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/var-persist/-/var-persist-1.0.1.tgz",
|
||||
"integrity": "sha512-Zon+pwvEpb0dEQCVShoMQQWV1JbWi4P2knW3h2sfSZS3pLecgbFig76tMSHEECQuEQ3KfYhMXgRDDIybtHTyZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||
@@ -3683,6 +3729,7 @@
|
||||
"version": "3.5.38",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
|
||||
"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.38",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"build": "vue-tsc --build && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ztimson/css-utils": "^1.3.5",
|
||||
"@ztimson/utils": "^0.29.5",
|
||||
"vue": "^3.5.38"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
import '@ztimson/css-utils/dist/css-utils.min.css';
|
||||
|
||||
const dark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { DataRow } from '../services/api'
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {api, BASE, type DataRow} from '../services/api';
|
||||
|
||||
const props = defineProps<{ data: DataRow }>()
|
||||
const d = computed(() => props.data)
|
||||
const data = ref<any>({});
|
||||
const precipitation = '';
|
||||
const humidity = computed(() => data.value.humidity || '—');
|
||||
const label = computed(() => data.value.forecast_weather_label || '');
|
||||
|
||||
const fmt = (v: unknown, dec = 1, unit = '') => v != null ? `${(v as number).toFixed(dec)}${unit}` : '—'
|
||||
const time = (v: unknown) => v ? new Date(v as string).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' }) : '—'
|
||||
onMounted(async () => {
|
||||
data.value = await api.current();
|
||||
console.log(data.value);
|
||||
});
|
||||
|
||||
const high = computed(() => fmt(d.value.env_temp_max_c, 1, '°C'))
|
||||
const low = computed(() => fmt(d.value.env_temp_min_c, 1, '°C'))
|
||||
const sunrise = computed(() => time(d.value.sun_sunrise))
|
||||
const sunset = computed(() => time(d.value.sun_sunset))
|
||||
const ghMornEnd = computed(() => time(d.value.sun_golden_hour_morning_end))
|
||||
const ghEveStart = computed(() => time(d.value.sun_golden_hour_evening_start))
|
||||
const dayLen = computed(() => d.value.sun_day_length_hours != null ? `${(d.value.sun_day_length_hours as number).toFixed(2)}h` : '—')
|
||||
const uviMax = computed(() => fmt(d.value.light_uvi_max, 1))
|
||||
const precipProb = computed(() => d.value.forecast_precipitation_probability != null ? `${d.value.forecast_precipitation_probability}%` : '—')
|
||||
const precipMm = computed(() => fmt(d.value.forecast_precipitation_mm, 1, 'mm'))
|
||||
const precipHrs = computed(() => d.value.precipitation_hours != null ? `${d.value.precipitation_hours}h` : null)
|
||||
const windMax = computed(() => fmt(d.value.wind_speed_max_kmh, 1, ' km/h'))
|
||||
const gustMax = computed(() => fmt(d.value.wind_gusts_max_kmh, 1, ' km/h'))
|
||||
const icon = computed(() => d.value.forecast_weather_icon as string | null)
|
||||
const label = computed(() => d.value.forecast_weather_label as string | null)
|
||||
const solarSum = computed(() => d.value.light_solar_wm2_sum != null ? `${(d.value.light_solar_wm2_sum as number).toFixed(1)} W/m²` : null)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -127,13 +115,16 @@ const solarSum = computed(() => d.value.light_solar_wm2_sum != null ? `${(d.va
|
||||
|
||||
<template>
|
||||
<div class="today-card">
|
||||
|
||||
<div class="today-header">
|
||||
<img v-if="icon" :src="icon" class="today-icon" alt="weather" />
|
||||
<div class="today-title">
|
||||
<div class="today-date">Today · {{ new Date().toLocaleDateString('en', { weekday: 'long', month: 'long', day: 'numeric' }) }}</div>
|
||||
<div class="today-label">{{ label }}</div>
|
||||
<div class="hi-lo">
|
||||
<div class="flex-r align-items-center justify-between px-3">
|
||||
<div class="flex-c justify-center align-items-center">
|
||||
<div class="today-label">{{ data.label }}</div>
|
||||
<img v-if="data.icon" :src="BASE + data.icon" class="today-icon" alt="weather" />
|
||||
</div>
|
||||
<div class="flex-r align-items-center gap-3">
|
||||
<div class="fs-6">
|
||||
0°C
|
||||
</div>
|
||||
<div class="flex-c fg-muted">
|
||||
<span class="hi">↑ {{ high }}</span>
|
||||
<span class="lo">↓ {{ low }}</span>
|
||||
</div>
|
||||
@@ -142,27 +133,13 @@ const solarSum = computed(() => d.value.light_solar_wm2_sum != null ? `${(d.va
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<!-- Sun -->
|
||||
<div class="section-label">☀️ Sun</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat"><span class="stat-label">Sunrise</span><span class="stat-value">{{ sunrise }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Sunset</span><span class="stat-value">{{ sunset }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Day Length</span><span class="stat-value">{{ dayLen }}</span></div>
|
||||
<div class="stat"><span class="stat-label">UV Max</span><span class="stat-value">{{ uviMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Golden AM</span><span class="stat-value">{{ ghMornEnd }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Golden PM</span><span class="stat-value">{{ ghEveStart }}</span></div>
|
||||
<div v-if="solarSum" class="stat"><span class="stat-label">Solar</span><span class="stat-value">{{ solarSum }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<!-- Precip & Wind -->
|
||||
<div class="section-label">🌧️ Precipitation & Wind</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat"><span class="stat-label">Chance</span><span class="stat-value">{{ precipProb }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Amount</span><span class="stat-value">{{ precipMm }}</span><span v-if="precipHrs" class="stat-sub">over {{ precipHrs }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Wind Max</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Gust Max</span><span class="stat-value">{{ gustMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Precipitation</span><span class="stat-value">{{ precipProb }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Humidity</span><span class="stat-value">{{ humidity }}</span><span v-if="precipHrs" class="stat-sub">over {{ precipHrs }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Wind</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Clouds</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">Air Quality</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
<div class="stat"><span class="stat-label">UV index</span><span class="stat-value">{{ windMax }}</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { DataRow } from '../services/api'
|
||||
import {BASE, type DataRow} from '../services/api';
|
||||
|
||||
const props = defineProps<{ days: DataRow[] }>()
|
||||
|
||||
@@ -57,7 +57,7 @@ const items = computed(() => props.days.slice(0, 7).map(d => ({
|
||||
<div class="strip">
|
||||
<div v-for="item in items" :key="item.date" class="day">
|
||||
<div class="day-name">{{ item.date }}</div>
|
||||
<img v-if="item.icon" :src="item.icon" class="day-icon" :alt="item.label" />
|
||||
<img v-if="item.icon" :src="BASE + item.icon" class="day-icon" :alt="item.label" />
|
||||
<div class="day-label">{{ item.label }}</div>
|
||||
<div class="day-temps">
|
||||
<span>{{ item.high }}</span>
|
||||
|
||||
@@ -177,8 +177,8 @@ function buildStationLayer(lat: number, lon: number, dark: boolean): VectorLayer
|
||||
onMounted(async () => {
|
||||
await fetchRadarFrames()
|
||||
|
||||
const lat = (current.value.gps_lat as number) || 0
|
||||
const lon = (current.value.gps_lon as number) || 0
|
||||
const lat = (current.value.latitude as number) || 0
|
||||
const lon = (current.value.longitude as number) || 0
|
||||
|
||||
stationLayer = buildStationLayer(lat, lon, props.dark)
|
||||
|
||||
@@ -221,16 +221,16 @@ onUnmounted(() => {
|
||||
watch(() => props.dark, dark => {
|
||||
map.getLayers().setAt(0, buildBaseLayer(dark))
|
||||
map.removeLayer(stationLayer)
|
||||
const lat = (current.value.gps_lat as number) || 0
|
||||
const lon = (current.value.gps_lon as number) || 0
|
||||
const lat = (current.value.latitude as number) || 0
|
||||
const lon = (current.value.longitude as number) || 0
|
||||
stationLayer = buildStationLayer(lat, lon, dark)
|
||||
map.addLayer(stationLayer)
|
||||
})
|
||||
|
||||
let positioned = false
|
||||
watch(() => current.value, () => {
|
||||
const lat = (current.value.gps_lat as number) || 0
|
||||
const lon = (current.value.gps_lon as number) || 0
|
||||
const lat = (current.value.latitude as number) || 0
|
||||
const lon = (current.value.longitude as number) || 0
|
||||
if (!positioned) {
|
||||
positioned = true
|
||||
map.getView().animate({ center: fromLonLat([lon, lat]), duration: 500 })
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
export const BASE = import.meta.env.DEV ? 'http://10.69.5.23:3000' : ''
|
||||
export const BASE = import.meta.env.DEV ? 'http://10.69.5.23' : ''
|
||||
|
||||
export type DataRow = Record<string, number | string | null>
|
||||
|
||||
export async function fetchDaily(start?: string, end?: string): Promise<DataRow[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.set('start', start)
|
||||
if (end) params.set('end', end)
|
||||
const res = await fetch(`/api/daily?${params}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
|
||||
const url = new URL(`${BASE}${path}`, window.location.origin)
|
||||
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))
|
||||
|
||||
@@ -9,26 +9,26 @@ export interface MetricMeta {
|
||||
|
||||
export const METRICS: Record<string, MetricMeta> = {
|
||||
// Environment
|
||||
env_temp_c: { label: 'Temperature', unit: '°C', icon: '🌡️', group: 'Environment', precision: 1, color: '#f97316' },
|
||||
temperature: { label: 'Temperature', unit: '°C', icon: '🌡️', group: 'Environment', precision: 1, color: '#f97316' },
|
||||
env_temp_f: { label: 'Temperature', unit: '°F', icon: '🌡️', group: 'Environment', precision: 1, color: '#f97316' },
|
||||
env_humidity: { label: 'Humidity', unit: '%', icon: '💧', group: 'Environment', precision: 1, color: '#38bdf8' },
|
||||
env_dew_point_c: { label: 'Dew Point', unit: '°C', icon: '🌫️', group: 'Environment', precision: 1, color: '#818cf8' },
|
||||
env_heat_index_c: { label: 'Feels Like', unit: '°C', icon: '🤔', group: 'Environment', precision: 1, color: '#fb923c' },
|
||||
env_pressure_hpa: { label: 'Pressure', unit: ' hPa', icon: '📊', group: 'Environment', precision: 1, color: '#a78bfa' },
|
||||
env_pressure_slp: { label: 'Sea Level Pressure', unit: ' hPa', icon: '📊', group: 'Environment', precision: 1, color: '#a78bfa' },
|
||||
env_abs_humidity: { label: 'Absolute Humidity', unit: ' g/m³', icon: '💦', group: 'Environment', precision: 2, color: '#67e8f9' },
|
||||
env_vpd_kpa: { label: 'Vapour Pressure Deficit', unit: ' kPa', icon: '🌿', group: 'Environment', precision: 2, color: '#4ade80' },
|
||||
env_aqi_score: { label: 'Air Quality', unit: '/100', icon: '🏭', group: 'Environment', precision: 0, color: '#34d399' },
|
||||
env_gas_ohms: { label: 'Gas Resistance', unit: ' Ω', icon: '⚗️', group: 'Environment', precision: 0, color: '#fbbf24' },
|
||||
humidity: { label: 'Humidity', unit: '%', icon: '💧', group: 'Environment', precision: 1, color: '#38bdf8' },
|
||||
dew_point: { label: 'Dew Point', unit: '°C', icon: '🌫️', group: 'Environment', precision: 1, color: '#818cf8' },
|
||||
heat_index: { label: 'Feels Like', unit: '°C', icon: '🤔', group: 'Environment', precision: 1, color: '#fb923c' },
|
||||
pressure_hpa: { label: 'Pressure', unit: ' hPa', icon: '📊', group: 'Environment', precision: 1, color: '#a78bfa' },
|
||||
pressure_slp: { label: 'Sea Level Pressure', unit: ' hPa', icon: '📊', group: 'Environment', precision: 1, color: '#a78bfa' },
|
||||
humidity_abs: { label: 'Absolute Humidity', unit: ' g/m³', icon: '💦', group: 'Environment', precision: 2, color: '#67e8f9' },
|
||||
vapor_pressure_deficit: { label: 'Vapour Pressure Deficit', unit: ' kPa', icon: '🌿', group: 'Environment', precision: 2, color: '#4ade80' },
|
||||
air_quality: { label: 'Air Quality', unit: '/100', icon: '🏭', group: 'Environment', precision: 0, color: '#34d399' },
|
||||
air_quality_ohms: { label: 'Gas Resistance', unit: ' Ω', icon: '⚗️', group: 'Environment', precision: 0, color: '#fbbf24' },
|
||||
// Light
|
||||
light_lux: { label: 'Illuminance', unit: ' lux', icon: '☀️', group: 'Light', precision: 0, color: '#fde68a' },
|
||||
light_uvi: { label: 'UV Index', unit: '', icon: '🕶️', group: 'Light', precision: 1, color: '#f472b6' },
|
||||
light_solar_wm2: { label: 'Solar Irradiance', unit: ' W/m²', icon: '⚡', group: 'Light', precision: 1, color: '#fcd34d' },
|
||||
light_uv_dose_mj: { label: 'UV Dose Today', unit: ' mJ/cm²', icon: '📡', group: 'Light', precision: 2, color: '#f9a8d4' },
|
||||
lux: { label: 'Illuminance', unit: ' lux', icon: '☀️', group: 'Light', precision: 0, color: '#fde68a' },
|
||||
uv_index: { label: 'UV Index', unit: '', icon: '🕶️', group: 'Light', precision: 1, color: '#f472b6' },
|
||||
solar_wm2: { label: 'Solar Irradiance', unit: ' W/m²', icon: '⚡', group: 'Light', precision: 1, color: '#fcd34d' },
|
||||
uv_dose: { label: 'UV Dose Today', unit: ' mJ/cm²', icon: '📡', group: 'Light', precision: 2, color: '#f9a8d4' },
|
||||
light_burn_time_min: { label: 'Burn Time', unit: ' min', icon: '🔥', group: 'Light', precision: 0, color: '#fb7185' },
|
||||
light_cloud_pct: { label: 'Cloud Cover', unit: '%', icon: '☁️', group: 'Light', precision: 0, color: '#94a3b8' },
|
||||
clouds: { label: 'Cloud Cover', unit: '%', icon: '☁️', group: 'Light', precision: 0, color: '#94a3b8' },
|
||||
light_dli: { label: 'Daily Light Integral', unit: ' mol/m²', icon: '🌱', group: 'Light', precision: 3, color: '#86efac' },
|
||||
light_visibility_km: { label: 'Visibility', unit: ' km', icon: '👁️', group: 'Light', precision: 1, color: '#7dd3fc' },
|
||||
visibility: { label: 'Visibility', unit: ' km', icon: '👁️', group: 'Light', precision: 1, color: '#7dd3fc' },
|
||||
// Wind
|
||||
wind_speed_kmh: { label: 'Wind Speed', unit: ' km/h', icon: '💨', group: 'Wind', precision: 1, color: '#93c5fd' },
|
||||
wind_gusts_kmh: { label: 'Wind Gusts', unit: ' km/h', icon: '🌬️', group: 'Wind', precision: 1, color: '#60a5fa' },
|
||||
@@ -41,11 +41,11 @@ export const METRICS: Record<string, MetricMeta> = {
|
||||
// Compass
|
||||
compass_heading: { label: 'Compass Heading', unit: '°', icon: '🧭', group: 'Compass', precision: 1, color: '#c084fc' },
|
||||
// Ground
|
||||
ground_distance_cm: { label: 'Ground Distance', unit: ' cm', icon: '📏', group: 'Ground', precision: 0, color: '#a3e635' },
|
||||
ground_accumulation_depth_cm: { label: 'Accumulation Depth', unit: ' cm', icon: '❄️', group: 'Ground', precision: 1, color: '#bfdbfe' },
|
||||
ground_lidar_strength: { label: 'LIDAR Strength', unit: '', icon: '📡', group: 'Ground', precision: 0, color: '#6ee7b7' },
|
||||
ground_distance: { label: 'Ground Distance', unit: ' cm', icon: '📏', group: 'Ground', precision: 0, color: '#a3e635' },
|
||||
accumulation: { label: 'Accumulation Depth', unit: ' cm', icon: '❄️', group: 'Ground', precision: 1, color: '#bfdbfe' },
|
||||
lidar_strength: { label: 'LIDAR Strength', unit: '', icon: '📡', group: 'Ground', precision: 0, color: '#6ee7b7' },
|
||||
// Lightning
|
||||
lightning_distance_km: { label: 'Lightning Distance', unit: ' km', icon: '⚡', group: 'Lightning', precision: 0, color: '#fde047' },
|
||||
lightning_distance: { label: 'Lightning Distance', unit: ' km', icon: '⚡', group: 'Lightning', precision: 0, color: '#fde047' },
|
||||
lightning_strikes_per_hour: { label: 'Strike Rate', unit: '/hr', icon: '⚡', group: 'Lightning', precision: 1, color: '#facc15' },
|
||||
// Moon
|
||||
moon_illumination_pct: { label: 'Moon Illumination', unit: '%', icon: '🌙', group: 'Celestial', precision: 0, color: '#e2e8f0' },
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import { current, hourly, daily, fetchAll, loading } from '../services/weather'
|
||||
import { METRICS, GROUPS } from '../services/units'
|
||||
import MapView from '../components/MapView.vue'
|
||||
import CurrentWeather from '../components/CurrentWeather.vue'
|
||||
import ForecastStrip from '../components/ForecastStrip.vue'
|
||||
import MetricCard from '../components/MetricCard.vue'
|
||||
import GraphModal from '../components/GraphModal.vue'
|
||||
`<script setup lang="ts">
|
||||
import {formatDate} from '@ztimson/utils';
|
||||
import {onMounted, onUnmounted, ref, computed} from 'vue';
|
||||
import {current, hourly, daily, fetchAll, loading} from '../services/weather';
|
||||
import {METRICS, GROUPS} from '../services/units';
|
||||
import MapView from '../components/MapView.vue';
|
||||
import CurrentWeather from '../components/CurrentWeather.vue';
|
||||
import ForecastStrip from '../components/ForecastStrip.vue';
|
||||
import MetricCard from '../components/MetricCard.vue';
|
||||
import GraphModal from '../components/GraphModal.vue';
|
||||
|
||||
const props = defineProps<{ dark: boolean }>()
|
||||
const selectedMetric = ref<string | null>(null)
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
const day = ref();
|
||||
const date = ref();
|
||||
const time = ref();
|
||||
const props = defineProps<{dark: boolean}>();
|
||||
const selectedMetric = ref<string | null>(null);
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
let clock: ReturnType<typeof setInterval>;
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAll()
|
||||
interval = setInterval(fetchAll, 60 * 1000)
|
||||
})
|
||||
clock = setInterval(() => {
|
||||
day.value = formatDate('dddd');
|
||||
date.value = formatDate('MMMM D');
|
||||
time.value = formatDate('h:mm A');
|
||||
}, 1000);
|
||||
await fetchAll();
|
||||
interval = setInterval(fetchAll, 60 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
onUnmounted(() => {
|
||||
clearInterval(clock);
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
const groupedMetrics = computed(() => {
|
||||
return GROUPS.map(group => ({
|
||||
@@ -25,8 +38,8 @@ const groupedMetrics = computed(() => {
|
||||
keys: Object.entries(METRICS)
|
||||
.filter(([key, meta]) => meta.group === group && current.value[key] != null)
|
||||
.map(([key]) => key)
|
||||
})).filter(g => g.keys.length > 0)
|
||||
})
|
||||
})).filter(g => g.keys.length > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -56,8 +69,14 @@ const groupedMetrics = computed(() => {
|
||||
overflow-y: auto;
|
||||
border-left: 1px solid var(--border);
|
||||
|
||||
&::-webkit-scrollbar { width: 4px; }
|
||||
&::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-label {
|
||||
@@ -78,7 +97,7 @@ const groupedMetrics = computed(() => {
|
||||
min-width: 0;
|
||||
|
||||
> * {
|
||||
flex: 1 1 140px; // grow, shrink, min width before wrapping
|
||||
flex: 1 1 140px; // grow, shrink, min width before wrapping
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -87,21 +106,27 @@ const groupedMetrics = computed(() => {
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; // ← added
|
||||
overflow-x: hidden; // ← added
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
@@ -119,46 +144,61 @@ const groupedMetrics = computed(() => {
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--border);
|
||||
overflow-y: visible;
|
||||
overflow-x: hidden; // ← added
|
||||
overflow-x: hidden; // ← added
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box; // ← added
|
||||
box-sizing: border-box; // ← added
|
||||
|
||||
&::before { display: none; }
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div v-if="loading" class="loading-bar" />
|
||||
<div class="dashboard">
|
||||
<div v-if="loading" class="loading-bar"/>
|
||||
|
||||
<div class="map-col">
|
||||
<MapView :dark="props.dark" />
|
||||
</div>
|
||||
<div class="map-col">
|
||||
<MapView :dark="props.dark"/>
|
||||
</div>
|
||||
|
||||
<div class="panel-col">
|
||||
<CurrentWeather :data="current" />
|
||||
<ForecastStrip :days="daily" />
|
||||
<div class="panel-col">
|
||||
<div class="flex-r align-items-center gap-3">
|
||||
<div class="pe-3" style="border-right: 2px solid var(--border)">
|
||||
<h2 class="fs-6 m-0" style="line-height: 1em">{{time}}</h2>
|
||||
</div>
|
||||
<div class="flex-c">
|
||||
<h1 class="m-0 fs-2" style="line-height: 1em">{{day}}</h1>
|
||||
<h2 class="m-0 fs-1 fg-muted" style="line-height: 1em">{{date}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<CurrentWeather />
|
||||
<ForecastStrip :days="daily"/>
|
||||
|
||||
<template v-for="g in groupedMetrics" :key="g.group">
|
||||
<div class="group-label">{{ g.group }}</div>
|
||||
<div class="metric-grid">
|
||||
<MetricCard
|
||||
v-for="key in g.keys"
|
||||
:key="key"
|
||||
:metric-key="key"
|
||||
:data="current"
|
||||
@click="selectedMetric = key"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template v-for="g in groupedMetrics" :key="g.group">
|
||||
<div class="group-label">{{ g.group }}</div>
|
||||
<div class="metric-grid">
|
||||
<MetricCard
|
||||
v-for="key in g.keys"
|
||||
:key="key"
|
||||
:metric-key="key"
|
||||
:data="current"
|
||||
@click="selectedMetric = key"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<GraphModal
|
||||
:metric-key="selectedMetric"
|
||||
:current-data="current"
|
||||
@close="selectedMetric = null"
|
||||
/>
|
||||
</div>
|
||||
<GraphModal
|
||||
:metric-key="selectedMetric"
|
||||
:current-data="current"
|
||||
@close="selectedMetric = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user