This commit is contained in:
2026-06-21 22:14:04 -04:00
commit 533aec8ba2
46 changed files with 3530 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<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'
const props = defineProps<{ dark: boolean }>()
const selectedMetric = ref<string | null>(null)
let interval: ReturnType<typeof setInterval>
onMounted(async () => {
await fetchAll()
interval = setInterval(fetchAll, 60 * 1000)
})
onUnmounted(() => clearInterval(interval))
// Group all available metric keys present in current data
const groupedMetrics = computed(() => {
return GROUPS.map(group => ({
group,
keys: Object.entries(METRICS)
.filter(([key, meta]) => meta.group === group && current.value[key] != null)
.map(([key]) => key)
})).filter(g => g.keys.length > 0)
})
</script>
<style scoped lang="scss">
.dashboard {
display: flex;
height: 100vh;
overflow: hidden;
background: var(--bg);
color: var(--text);
}
.map-col {
flex: 1;
min-width: 0;
padding: 16px;
display: flex;
flex-direction: column;
}
.panel-col {
width: 380px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
overflow-y: auto;
border-left: 1px solid var(--border);
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
}
.group-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-top: 4px;
margin-bottom: 2px;
padding: 0 2px;
}
.metric-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.loading-bar {
height: 2px;
background: var(--accent);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@media (max-width: 768px) {
.dashboard { flex-direction: column; }
.map-col { height: 45vh; padding: 8px; }
.panel-col { width: 100%; border-left: none; border-top: 1px solid var(--border); }
.metric-grid { grid-template-columns: 1fr 1fr; }
}
</style>
<template>
<div class="dashboard">
<div v-if="loading" class="loading-bar" />
<div class="map-col">
<MapView :dark="props.dark" />
</div>
<div class="panel-col">
<CurrentWeather :data="current" />
<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>
<GraphModal
:metric-key="selectedMetric"
:current-data="current"
@close="selectedMetric = null"
/>
</div>
</template>