feat(briefing): add WeatherCard.vue component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface ForecastDay {
|
||||||
|
day: string
|
||||||
|
condition: string
|
||||||
|
high: number
|
||||||
|
low: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeatherData {
|
||||||
|
location: string
|
||||||
|
fetched_at: string
|
||||||
|
current_temp: number
|
||||||
|
condition: string
|
||||||
|
today_high: number | null
|
||||||
|
today_low: number | null
|
||||||
|
yesterday_high: number | null
|
||||||
|
yesterday_low: number | null
|
||||||
|
forecast: ForecastDay[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
weather: WeatherData | null
|
||||||
|
tempUnit?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const unit = computed(() => props.tempUnit ?? 'C')
|
||||||
|
|
||||||
|
const tempDelta = computed(() => {
|
||||||
|
const w = props.weather
|
||||||
|
if (!w || w.today_high == null || w.yesterday_high == null) return null
|
||||||
|
const diff = w.today_high - w.yesterday_high
|
||||||
|
if (Math.abs(diff) < 1) return 'Same as yesterday'
|
||||||
|
const dir = diff > 0 ? 'warmer' : 'cooler'
|
||||||
|
return `${Math.abs(diff)}° ${dir} than yesterday`
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchedAtLabel = computed(() => {
|
||||||
|
if (!props.weather?.fetched_at) return ''
|
||||||
|
try {
|
||||||
|
return new Date(props.weather.fetched_at).toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="weather" class="weather-card">
|
||||||
|
<div class="weather-header">
|
||||||
|
<span class="weather-location">{{ weather.location }}</span>
|
||||||
|
<span class="weather-fetched-at">as of {{ fetchedAtLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-current">
|
||||||
|
<span class="weather-temp">{{ weather.current_temp }}°{{ unit }}</span>
|
||||||
|
<span class="weather-condition">{{ weather.condition }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-today" v-if="weather.today_high != null">
|
||||||
|
Today: {{ weather.today_high }}° / {{ weather.today_low }}°
|
||||||
|
<span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-forecast" v-if="weather.forecast.length">
|
||||||
|
<div v-for="day in weather.forecast" :key="day.day" class="weather-forecast-day">
|
||||||
|
<span class="forecast-day-name">{{ day.day }}</span>
|
||||||
|
<span class="forecast-condition">{{ day.condition }}</span>
|
||||||
|
<span class="forecast-temps">{{ day.high }}° / {{ day.low }}°</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="weather-card weather-unavailable">
|
||||||
|
Weather data unavailable — will retry at next slot.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.weather-card {
|
||||||
|
background: color-mix(in srgb, var(--color-surface) 80%, transparent);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-location {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-fetched-at {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-temp {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-condition {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-today {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-delta {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast-day {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-day-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-condition {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-temps {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-unavailable {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user