Fixed up sensor cards

This commit is contained in:
2026-06-24 23:32:45 -04:00
parent 8bece21ddb
commit 08319ce277
12 changed files with 717 additions and 139 deletions

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import Loading from '@/components/Loading.vue';
import { inject, onMounted, ref, computed } from 'vue';
import { api } from '../services/api';
const data = ref<any>(null);
const openGraph = inject<(key: string) => void>('openGraph');
onMounted(async () => data.value = await api.current());
const W = 320, H = 120, HORIZON = 45;
const arc = computed(() => {
if (!data.value) return { filledPath: '', remainPath: '', cx: 0, cy: 0, pct: 0 };
const rise = new Date(data.value.sunrise).getTime();
const set = new Date(data.value.sunset).getTime();
const now = Date.now();
const dayLen = set - rise;
const noon = rise + dayLen / 2;
const start = noon - 12 * 3600 * 1000;
const end = noon + 12 * 3600 * 1000;
const pct = Math.max(0, Math.min(1, (now - start) / (end - start)));
const x1 = 0, x2 = W;
const totalW = x2 - x1;
// Sine curve: below horizon at edges, peaks above at noon
const getPoint = (t: number) => ({
x: x1 + totalW * t,
y: HORIZON - Math.sin(t * Math.PI * 2 - Math.PI / 2) * (HORIZON) + (HORIZON)
// Simpler: just a single-hump arc above horizon + dip below
});
// Generate points along full sine path
const pts = Array.from({ length: 201 }, (_, i) => {
const t = i / 200;
const x = x1 + totalW * t;
// Single smooth S: -sin gives us below->above->below
const amplitude = (HORIZON);
const y = HORIZON - Math.sin(t * Math.PI * 2 - Math.PI / 2) * amplitude;
return { x, y };
});
const splitIdx = Math.round(pct * 200);
const { x: cx, y: cy } = pts[splitIdx];
// Filled traveled path: close down to horizon baseline for fill
const traveledPts = pts.slice(0, splitIdx + 1);
const filledPath = traveledPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ')
+ ` L${traveledPts[traveledPts.length - 1].x.toFixed(1)},${HORIZON}`
+ ` L${x1},${HORIZON} Z`;
// Remaining path stroke only
const remainPts = pts.slice(splitIdx);
const remainPath = remainPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
// Sunrise / sunset x positions
const riseX = x1 + totalW * ((rise - start) / (end - start));
const setX = x1 + totalW * ((set - start) / (end - start));
const noonX = x1 + totalW * 0.5;
return { filledPath, remainPath, cx, cy, pct, riseX, setX, noonX };
});
</script>
<template>
<div class="sun-card">
<div class="card-title">🌅 Sun</div>
<!-- Top labels -->
<div class="sun-top-labels" v-if="data">
<div>
<div class="stat-label">Sunrise</div>
<div class="stat-value">{{ new Date(data.sunrise).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
<div style="text-align:right">
<div class="stat-label">Sunset</div>
<div class="stat-value">{{ new Date(data.sunset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
</div>
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
<!-- Filled traveled area -->
<path v-if="arc.filledPath" :d="arc.filledPath" fill="#3b82f6" opacity="0.35" />
<!-- Remaining curve stroke -->
<path v-if="arc.remainPath" :d="arc.remainPath" fill="none" stroke="#94a3b8" stroke-width="1.5" opacity="0.5" />
<!-- Horizon line -->
<line :x1="0" :y1="HORIZON" :x2="W" :y2="HORIZON" stroke="var(--border)" stroke-width="1" />
<!-- Sun dot -->
<circle v-if="data" :cx="arc.cx" :cy="arc.cy" r="8" fill="#f59e0b" class="sun-dot" />
</svg>
<div class="divider" />
<div class="stat-grid">
<div class="stat">
<span class="stat-label">Daylight</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.daylight?.toFixed(2) }} <span class="fg-muted">hrs</span></span>
</div>
<div class="stat" @click="openGraph?.('lux')">
<span class="stat-label">Lux</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.lux }}</span>
</div>
<div class="stat" @click="openGraph?.('solar_wm2')">
<span class="stat-label">Solar W/</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.solar_wm2 }}</span>
</div>
</div>
<div class="divider" />
<div class="stat-grid">
<div class="stat" @click="openGraph?.('uv_index')">
<span class="stat-label">UV Index</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.uv_index.toFixed(1) }} <span class="fg-muted">({{ data.uv_index_label }})</span></span>
</div>
<div class="stat" @click="openGraph?.('uv_dose')">
<span class="stat-label">UV Dose</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.uv_dose?.toFixed(0) }} <span class="fg-muted">J/</span></span>
</div>
<div class="stat" @click="openGraph?.('uv_index_max')">
<span class="stat-label">UV Max</span>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden" style="height: 1.75em"><Loading /></div>
<span v-else class="stat-value">{{ data.uv_index_max ?? '—' }}</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.sun-card {
position: relative;
border-radius: 12px;
background: var(--surface);
border: 1px solid var(--border);
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sun-top-labels {
display: flex;
justify-content: space-between;
padding: 0 4px;
}
.card-title {
font-size: 13px;
font-weight: 700;
color: var(--text);
}
.divider {
height: 1px;
background: var(--border);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 10px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
cursor: pointer;
border-radius: 8px;
padding: 4px 6px;
margin: -4px -6px;
transition: background 0.15s;
&:hover { background: var(--hover); }
}
.stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: var(--text);
}
svg {
width: 100%;
height: 120px;
overflow: visible;
}
.sun-dot {
filter: drop-shadow(0 0 5px #f59e0b88);
}
</style>