dashboard sleep widget: fill empty space with hypnogram graph from Health tab
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import api from '../utils/api'
|
||||
import { useIsMobile } from '../hooks/useMediaQuery'
|
||||
import StatCard from '../components/ui/StatCard'
|
||||
import HrvBadge from '../components/ui/HrvBadge'
|
||||
import SleepHypnogram from '../components/health/SleepHypnogram'
|
||||
import ActivityMap from '../components/activity/ActivityMap'
|
||||
import {
|
||||
formatDuration, formatDistance, formatHeartRate, formatElevation,
|
||||
@@ -237,38 +238,48 @@ const SLEEP_STAGES = [
|
||||
{ key: 'sleep_awake_s', label: 'Awake', color: '#6b7280' },
|
||||
]
|
||||
|
||||
function SleepDetail({ health }) {
|
||||
function SleepDetail({ health, sleepStages }) {
|
||||
const total = SLEEP_STAGES.reduce((s, st) => s + (health[st.key] || 0), 0)
|
||||
const hasHypnogram = health.sleep_start && health.sleep_end && sleepStages?.length
|
||||
return (
|
||||
<Card title="Sleep" viewHref="/health">
|
||||
<div className="flex items-baseline gap-3 flex-wrap">
|
||||
<span className="text-3xl font-bold text-indigo-300">{formatSleep(health.sleep_duration_s)}</span>
|
||||
{health.sleep_score != null && (
|
||||
<span className="text-sm text-gray-400">score <span className="text-white font-semibold">{Math.round(health.sleep_score)}</span></span>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-baseline gap-3 flex-wrap">
|
||||
<span className="text-3xl font-bold text-indigo-300">{formatSleep(health.sleep_duration_s)}</span>
|
||||
{health.sleep_score != null && (
|
||||
<span className="text-sm text-gray-400">score <span className="text-white font-semibold">{Math.round(health.sleep_score)}</span></span>
|
||||
)}
|
||||
</div>
|
||||
{total > 0 ? (
|
||||
<>
|
||||
<div className="flex h-3 rounded-full overflow-hidden gap-0.5 mt-3">
|
||||
{SLEEP_STAGES.map(st => {
|
||||
const pct = ((health[st.key] || 0) / total) * 100
|
||||
if (pct < 0.5) return null
|
||||
return <div key={st.key} style={{ width: `${pct}%`, backgroundColor: st.color }} title={`${st.label}: ${formatSleep(health[st.key])}`} />
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-2">
|
||||
{SLEEP_STAGES.map(st => (health[st.key] ? (
|
||||
<div key={st.key} className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: st.color }} />
|
||||
<span className="text-xs text-gray-400">{st.label}</span>
|
||||
<span className="text-xs text-white">{formatSleep(health[st.key])}</span>
|
||||
</div>
|
||||
) : null))}
|
||||
</div>
|
||||
{hasHypnogram && (
|
||||
<div className="flex-1 flex items-center mt-4">
|
||||
<div className="w-full">
|
||||
<SleepHypnogram sleepStart={health.sleep_start} sleepEnd={health.sleep_end} stages={sleepStages} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-gray-600 mt-3">No sleep stages for last night</p>
|
||||
)}
|
||||
</div>
|
||||
{total > 0 ? (
|
||||
<>
|
||||
<div className="flex h-3 rounded-full overflow-hidden gap-0.5 mt-3">
|
||||
{SLEEP_STAGES.map(st => {
|
||||
const pct = ((health[st.key] || 0) / total) * 100
|
||||
if (pct < 0.5) return null
|
||||
return <div key={st.key} style={{ width: `${pct}%`, backgroundColor: st.color }} title={`${st.label}: ${formatSleep(health[st.key])}`} />
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-2">
|
||||
{SLEEP_STAGES.map(st => (health[st.key] ? (
|
||||
<div key={st.key} className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: st.color }} />
|
||||
<span className="text-xs text-gray-400">{st.label}</span>
|
||||
<span className="text-xs text-white">{formatSleep(health[st.key])}</span>
|
||||
</div>
|
||||
) : null))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-gray-600 mt-3">No sleep stages for last night</p>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -572,7 +583,7 @@ export default function DashboardPage() {
|
||||
case 'weekly': return <WeeklyChart activities={allActivities} />
|
||||
case 'bodyBattery': return <BodyBatteryToday bb={intraday?.body_battery} hires={intraday?.body_battery_hires} sleepStart={health.sleep_start} sleepEnd={health.sleep_end} />
|
||||
case 'vo2maxTrend': return <Vo2MaxTrend health={health} recentHealth={recentHealth} />
|
||||
case 'sleepDetail': return <SleepDetail health={health} />
|
||||
case 'sleepDetail': return <SleepDetail health={health} sleepStages={intraday?.sleep_stages} />
|
||||
case 'weight': return <WeightMini recentHealth={recentHealth} />
|
||||
case 'featured': return <FeaturedActivity activity={featured} segments={featuredSegments} />
|
||||
case 'recent': return <RecentActivities activities={recentActivities} />
|
||||
|
||||
Reference in New Issue
Block a user