From 5ce45813c7aa72e18f7704ae4bc2a27a8179293e Mon Sep 17 00:00:00 2001 From: James Rhee Date: Mon, 9 Mar 2026 17:37:59 -0700 Subject: [PATCH 1/2] Redesign home screen with Throne uroflow and HealthKit wellness dashboard - Throne Science module: metric toggles (Avg Flow/Volume/Duration/Max Flow), 7-day bar chart via bucketSeries, real synced data from fetchSessions+fetchMetricsBatch - Apple HealthKit module: three concentric activity rings (Move/Exercise/Steps), sleep quality, heart rate, SpO2, respiratory rate via useHealthSummary() - Preserved all existing functionality (surgery date, study timeline, watch reminder, dev tools modal, SurgeryCompleteModal, pull-to-refresh) - Fixed data fetch to match voiding.tsx pattern (no userId filter, no status filter) Co-Authored-By: Claude Sonnet 4.6 --- homeflow/app/(tabs)/index.tsx | 956 ++++++++++++++++++++++++++-------- 1 file changed, 745 insertions(+), 211 deletions(-) diff --git a/homeflow/app/(tabs)/index.tsx b/homeflow/app/(tabs)/index.tsx index 1fb63b5..0ae6f5a 100644 --- a/homeflow/app/(tabs)/index.tsx +++ b/homeflow/app/(tabs)/index.tsx @@ -1,13 +1,27 @@ -import React, { useState } from 'react'; +/** + * Home Screen — StreamSync Wellness Dashboard + * + * Displays: + * - Throne Science uroflow module (7-day chart with metric toggle) + * - Apple HealthKit module (activity rings, sleep, vitals) + * - Surgery date & study timeline + * - Watch wear reminder + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + ActivityIndicator, + Dimensions, + Modal, + Platform, + Pressable, + RefreshControl, + ScrollView, StyleSheet, - View, Text, - ScrollView, TouchableOpacity, + View, Alert, - Modal, - Pressable, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; @@ -18,15 +32,495 @@ import { useSurgeryDate } from '@/hooks/use-surgery-date'; import { useWatchUsage } from '@/hooks/use-watch-usage'; import { SurgeryCompleteModal } from '@/components/home/SurgeryCompleteModal'; import { useAppTheme } from '@/lib/theme/ThemeContext'; +import { useAuth } from '@/lib/auth/auth-context'; +import { useHealthSummary } from '@/hooks/use-health-summary'; +import { + fetchSessions, + fetchMetricsBatch, + type ThroneSession, + type ThroneMetric, +} from '@/src/services/throneFirestore'; +import { + parseSessionWithMetrics, + type ParsedVoidSession, +} from '@/src/data/parseVoidingSession'; +import { + filterByRange, + bucketSeries, + computeSummaryStats, + type BucketPoint, +} from '@/src/data/voidingAggregation'; +import { + METRIC_LABELS, + METRIC_UNITS, + METRIC_KEYS, + type VoidMetricKey, +} from '@/src/data/voidingFieldMap'; + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const WEEK_MS = 7 * 24 * 60 * 60 * 1000; + +const MOVE_GOAL_KCAL = 500; +const EXERCISE_GOAL_MIN = 30; +const STEPS_GOAL = 10_000; + +// ─── Activity Ring Component ────────────────────────────────────────────────── + +/** + * Draws a single circular progress ring using the two-half-disc technique. + * Works without any SVG library. + */ +function CircleRing({ + pct, + color, + size, + strokeWidth, + bgColor, +}: { + pct: number; + color: string; + size: number; + strokeWidth: number; + bgColor: string; +}) { + const r = Math.max(0, Math.min(1, isNaN(pct) ? 0 : pct)); + const half = size / 2; + const innerSize = size - strokeWidth * 2; + + const rightDeg = r <= 0.5 ? -180 + r * 360 : 0; + const leftDeg = r <= 0.5 ? -180 : -180 + (r - 0.5) * 360; + + return ( + + {/* Gray track disc */} + + + {/* Right half fill */} + + + + + {/* Left half fill — only shown past 50% */} + {r > 0.5 && ( + + + + )} + + {/* Inner cutout to create ring appearance */} + + + ); +} + +/** Three concentric activity rings (Move / Exercise / Steps). */ +function ActivityRings({ + movePct, + exercisePct, + stepsPct, + cardColor, +}: { + movePct: number; + exercisePct: number; + stepsPct: number; + cardColor: string; +}) { + const STROKE = 10; + const GAP = 3; + const OUTER = 86; + const MIDDLE = OUTER - STROKE * 2 - GAP * 2; + const INNER = MIDDLE - STROKE * 2 - GAP * 2; + + const outerCenter = OUTER / 2; + const middleOffset = STROKE + GAP; + const innerOffset = STROKE * 2 + GAP * 2; + + return ( + + + + + + + + + + ); +} + +// ─── Mini bar chart ─────────────────────────────────────────────────────────── + +function MiniBarChart({ + data, + accentColor, + c, +}: { + data: BucketPoint[]; + accentColor: string; + c: ReturnType['theme']['colors']; +}) { + const CHART_HEIGHT = 80; + const screenW = Dimensions.get('window').width; + const chartW = screenW - 32 * 2 - 16 * 2 - 36; // screen - screenPad*2 - cardPad*2 - yAxis + + if (data.length === 0) { + return ( + + + No data this week + + + ); + } + + const maxVal = Math.max(...data.map((d) => d.value), 0.01); + const barW = Math.max(6, Math.floor((chartW - (data.length - 1) * 4) / data.length)); + + return ( + + + {/* Y-axis */} + + + {maxVal.toFixed(1)} + + 0 + + + {/* Bars */} + + + + + + {data.map((pt, i) => ( + + + + ))} + + + + + {/* X labels */} + + {data.map((pt, i) => { + const show = + data.length <= 7 || + i === 0 || + i === data.length - 1 || + i % Math.ceil(data.length / 5) === 0; + return ( + + {show && ( + + {pt.label} + + )} + + ); + })} + + + ); +} + +const miniChartStyles = StyleSheet.create({ + chartRow: { flexDirection: 'row', alignItems: 'stretch' }, + yAxis: { width: 36, justifyContent: 'space-between', paddingRight: 6, alignItems: 'flex-end' }, + axisLabel: { fontSize: 9 }, + barArea: { flex: 1, position: 'relative' }, + gridLine: { + position: 'absolute', + left: 0, + right: 0, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + barRow: { flex: 1, flexDirection: 'row', alignItems: 'flex-end', gap: 4 }, + barWrapper: { flex: 1, alignItems: 'center', justifyContent: 'flex-end', height: '100%' }, + xRow: { flexDirection: 'row', marginTop: 4 }, + xLabel: { fontSize: 9 }, + empty: { justifyContent: 'center', alignItems: 'center' }, + emptyText: { fontSize: 13 }, +}); + +// ─── Metric pill row ────────────────────────────────────────────────────────── + +function MetricPills({ + selected, + onSelect, + c, +}: { + selected: VoidMetricKey; + onSelect: (k: VoidMetricKey) => void; + c: ReturnType['theme']['colors']; +}) { + return ( + + {METRIC_KEYS.map((key) => { + const active = key === selected; + return ( + onSelect(key)} + style={[ + pillStyles.pill, + { backgroundColor: active ? c.accent : c.secondaryFill }, + ]} + > + + {METRIC_LABELS[key]} + + + ); + })} + + ); +} + +const pillStyles = StyleSheet.create({ + row: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 }, + pill: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 20 }, + text: { fontSize: 12, fontWeight: '600' }, +}); + +// ─── Vitals row ─────────────────────────────────────────────────────────────── + +function VitalRow({ + icon, + label, + value, + color, + c, + isLast, +}: { + icon: string; + label: string; + value: string; + color: string; + c: ReturnType['theme']['colors']; + isLast?: boolean; +}) { + return ( + <> + + + + + {label} + {value} + + {!isLast && } + + ); +} + +const vitalStyles = StyleSheet.create({ + row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, gap: 10 }, + iconBox: { width: 30, height: 30, borderRadius: 8, alignItems: 'center', justifyContent: 'center' }, + label: { flex: 1, fontSize: 15 }, + value: { fontSize: 15, fontWeight: '600' }, + divider: { height: StyleSheet.hairlineWidth, marginLeft: 40 }, +}); + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function formatMetricSummary(sessions: ParsedVoidSession[], metric: VoidMetricKey): string { + const vals = sessions.map((s) => s[metric]).filter((v): v is number => v !== null); + if (vals.length === 0) return '—'; + const avg = vals.reduce((a, b) => a + b, 0) / vals.length; + if (metric === 'durationSeconds') { + const sec = Math.round(avg); + if (sec >= 60) return `${Math.floor(sec / 60)}m ${sec % 60}s`; + return `${sec}s`; + } + return avg.toFixed(1); +} + +function daysUntil(dateStr: string): string { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const target = new Date(dateStr + 'T00:00:00'); + const diff = Math.ceil((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + if (diff === 0) return 'Scheduled for today'; + if (diff === 1) return 'Tomorrow'; + if (diff < 0) return ''; + return `${diff} days from now`; +} + +// ─── Main Screen ────────────────────────────────────────────────────────────── export default function HomeScreen() { const { theme } = useAppTheme(); const { isDark, colors: t } = theme; + const { user } = useAuth(); const surgery = useSurgeryDate(); const watch = useWatchUsage(); + const [showSurgeryModal, setShowSurgeryModal] = useState(false); const [showDevSheet, setShowDevSheet] = useState(false); const [watchDismissed, setWatchDismissed] = useState(false); + const [refreshKey, setRefreshKey] = useState(0); + + // ─── Throne data ─────────────────────────────────────────────────────────── + + const [sessions, setSessions] = useState([]); + const [throneLoading, setThroneLoading] = useState(true); + const [selectedMetric, setSelectedMetric] = useState('avgFlowRate'); + + useEffect(() => { + let cancelled = false; + + async function loadThroneData() { + setThroneLoading(true); + try { + const since = new Date(Date.now() - WEEK_MS); + // Match voiding.tsx: no userId filter (Firestore userId field may differ from auth uid) + const raw: ThroneSession[] = await fetchSessions({ startDate: since }); + if (cancelled) return; + + const ids = raw.map((s) => s.id); + const allMetrics: ThroneMetric[] = await fetchMetricsBatch(ids); + if (cancelled) return; + + // Build sessionId → ThroneMetric[] map (same as voiding.tsx) + const metricsMap = new Map(); + for (const m of allMetrics) { + const arr = metricsMap.get(m.sessionId); + if (arr) arr.push(m); + else metricsMap.set(m.sessionId, [m]); + } + + // Keep all sessions (no status filter) — same as voiding.tsx + const parsed: ParsedVoidSession[] = raw + .map((s) => parseSessionWithMetrics(s, metricsMap.get(s.id) ?? [])); + + if (!cancelled) setSessions(parsed); + } catch { + // silent — empty state shows + } finally { + if (!cancelled) setThroneLoading(false); + } + } + + loadThroneData(); + return () => { cancelled = true; }; + }, [refreshKey]); + + const weekSessions = useMemo( + () => filterByRange(sessions, '1w'), + [sessions], + ); + + const chartData = useMemo( + () => bucketSeries(weekSessions, '1w', selectedMetric), + [weekSessions, selectedMetric], + ); + + const metricAvg = useMemo( + () => formatMetricSummary(weekSessions, selectedMetric), + [weekSessions, selectedMetric], + ); + + // ─── HealthKit data ──────────────────────────────────────────────────────── + + const { summary: health, isLoading: healthLoading, refresh: refreshHealth } = useHealthSummary(); + + const activity = health?.activity ?? null; + const sleep = health?.raw.sleep ?? null; + const vitals = health?.raw.vitals ?? null; + + const movePct = activity ? Math.min(1, activity.energyBurned / MOVE_GOAL_KCAL) : 0; + const exercisePct = activity ? Math.min(1, activity.activeMinutes / EXERCISE_GOAL_MIN) : 0; + const stepsPct = activity ? Math.min(1, activity.steps / STEPS_GOAL) : 0; + + const sleepLabel = sleep + ? `${Math.floor(sleep.totalAsleepMinutes / 60)}h ${sleep.totalAsleepMinutes % 60}m` + : '—'; + + const hrLabel = vitals?.heartRate?.average + ? `${Math.round(vitals.heartRate.average)} bpm` + : vitals?.restingHeartRate + ? `${Math.round(vitals.restingHeartRate)} bpm` + : '—'; + + const spo2Label = vitals?.oxygenSaturation + ? `${Math.round(vitals.oxygenSaturation * 100)}%` + : '—'; + + const respLabel = vitals?.respiratoryRate + ? `${Math.round(vitals.respiratoryRate)} brpm` + : '—'; + + // ─── Refresh ─────────────────────────────────────────────────────────────── + + const onRefresh = useCallback(() => { + setRefreshKey((k) => k + 1); + refreshHealth(); + }, [refreshHealth]); const handleResetOnboarding = () => { Alert.alert( @@ -41,8 +535,7 @@ export default function HomeScreen() { try { await OnboardingService.reset(); notifyOnboardingComplete(); - } catch (error) { - console.error('Error resetting onboarding:', error); + } catch { Alert.alert('Error', 'Failed to reset onboarding'); } }, @@ -53,14 +546,23 @@ export default function HomeScreen() { const showWatchReminder = !watch.isLoading && !watch.watchWornRecently && !watchDismissed; + // ─── Render ──────────────────────────────────────────────────────────────── + return ( + } > - {/* Header — Apple Health style: date above, large title below */} + {/* Header */} @@ -89,13 +591,159 @@ export default function HomeScreen() { )} - {/* Surgery Date Card */} + {/* ─── Throne Science Module ─────────────────────────────────────── */} + + + + + + + + + Throne Science + + + Uroflow · Past 7 days + + + + + + {/* Metric toggles */} + + + {/* Summary stat */} + + + {throneLoading ? '—' : metricAvg} + + + {' '}{METRIC_UNITS[selectedMetric]} + + + {' '}avg {METRIC_LABELS[selectedMetric].toLowerCase()} + + + + {/* Bar chart */} + {throneLoading ? ( + + + + ) : ( + + )} + + + {/* ─── Apple HealthKit Module ────────────────────────────────────── */} + + + + + + + + + Apple Health + + + Today's summary + + + + + + {healthLoading ? ( + + + + ) : Platform.OS !== 'ios' ? ( + + Apple Health is only available on iOS. + + ) : ( + <> + {/* Activity rings + stats */} + + + + + + + + Move + + + {activity ? `${Math.round(activity.energyBurned)} kcal` : '—'} + + + + + + Exercise + + + {activity ? `${activity.activeMinutes} min` : '—'} + + + + + + Steps + + + {activity ? activity.steps.toLocaleString() : '—'} + + + + + + + + {/* Sleep, Heart Rate, SpO2, Respiratory */} + + + + + + )} + + + {/* ─── Surgery Date Card ─────────────────────────────────────────── */} - - Surgery date - + Surgery date {surgery.isPlaceholder && __DEV__ && ( @@ -125,14 +773,12 @@ export default function HomeScreen() { )} - {/* Study Timeline Card */} + {/* Study Timeline */} {!surgery.isLoading && ( - - Study timeline - + Study timeline {surgery.isPlaceholder && __DEV__ && ( @@ -143,18 +789,14 @@ export default function HomeScreen() { - - Start - + Start {surgery.studyStartLabel} - - End - + End {surgery.studyEndLabel} @@ -163,7 +805,7 @@ export default function HomeScreen() { )} - {/* Watch Reminder */} + {/* Watch reminder */} {showWatchReminder && ( @@ -196,7 +838,7 @@ export default function HomeScreen() { )} - {/* Recovery Instructions card — shown after surgery */} + {/* Recovery card */} {surgery.hasPassed && ( Recovery plan - + - - Your Recovery Plan - + Your Recovery Plan Stanford discharge instructions · tap to review )} - {/* Study info */} - - - Your Study - - - Track your BPH symptoms, voiding patterns, and recovery progress - throughout the study. Your daily data helps your care team - understand your health patterns. - - - @@ -252,9 +885,7 @@ export default function HomeScreen() { onPress={() => {}} > - - Developer Tools - + Developer Tools setShowDevSheet(false)} activeOpacity={0.7} > - - Close - + Close @@ -301,57 +930,24 @@ export default function HomeScreen() { ); } -function daysUntil(dateStr: string): string { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const target = new Date(dateStr + 'T00:00:00'); - const diff = Math.ceil((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); - if (diff === 0) return 'Scheduled for today'; - if (diff === 1) return 'Tomorrow'; - if (diff < 0) return ''; - return `${diff} days from now`; -} +// ─── Styles ─────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ - container: { - flex: 1, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: 16, - paddingTop: 8, - }, + container: { flex: 1 }, + scrollView: { flex: 1 }, + scrollContent: { paddingHorizontal: 16, paddingTop: 8 }, - // Header — matches Apple Health large-title pattern + // Header headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 20, }, - headerText: { - flex: 1, - }, - dateLabel: { - fontSize: 13, - fontWeight: '400', - marginBottom: 2, - }, - greetingNormal: { - fontSize: 34, - fontWeight: '700', - letterSpacing: 0.37, - }, - greeting: { - fontSize: 34, - fontWeight: '700', - letterSpacing: 0.37, - fontStyle: 'italic', - }, - - // Dev pill + headerText: { flex: 1 }, + dateLabel: { fontSize: 13, fontWeight: '400', marginBottom: 2 }, + greetingNormal: { fontSize: 34, fontWeight: '700', letterSpacing: 0.37 }, + greeting: { fontSize: 34, fontWeight: '700', letterSpacing: 0.37, fontStyle: 'italic' }, devPill: { flexDirection: 'row', alignItems: 'center', @@ -361,102 +957,73 @@ const styles = StyleSheet.create({ borderRadius: 14, marginTop: 20, }, - devPillText: { - fontSize: 12, - fontWeight: '500', - }, + devPillText: { fontSize: 12, fontWeight: '500' }, - // Cards — iOS grouped-style - card: { - borderRadius: 12, + // Module cards (Throne + HealthKit) + moduleCard: { + borderRadius: 16, padding: 16, marginBottom: 12, + gap: 12, }, - cardHeader: { + moduleHeader: { flexDirection: 'row', alignItems: 'center', - gap: 6, - marginBottom: 8, + justifyContent: 'space-between', }, - cardLabel: { - fontSize: 13, - fontWeight: '600', - letterSpacing: 0.2, + moduleHeaderLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 }, + moduleIconBox: { + width: 36, + height: 36, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', }, + moduleTitle: { fontSize: 17, fontWeight: '700' }, + moduleSubtitle: { fontSize: 12, marginTop: 1 }, + + // Metric summary + metricSummaryRow: { flexDirection: 'row', alignItems: 'baseline' }, + metricSummaryValue: { fontSize: 28, fontWeight: '700' }, + metricSummaryUnit: { fontSize: 15, fontWeight: '500' }, + metricSummaryLabel: { fontSize: 13 }, + + // Loading + loadingRow: { height: 80, alignItems: 'center', justifyContent: 'center' }, + platformNote: { fontSize: 14, fontStyle: 'italic', textAlign: 'center', paddingVertical: 12 }, + + // Activity rings section + activityRow: { flexDirection: 'row', alignItems: 'center', gap: 20 }, + activityStats: { flex: 1, gap: 10 }, + activityStatRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + ringDot: { width: 8, height: 8, borderRadius: 4 }, + activityStatLabel: { flex: 1, fontSize: 13 }, + activityStatValue: { fontSize: 13, fontWeight: '600' }, + sectionDivider: { height: StyleSheet.hairlineWidth, marginVertical: 4 }, + + // Legacy cards (surgery, timeline, etc.) + card: { borderRadius: 12, padding: 16, marginBottom: 12 }, + cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8 }, + cardLabel: { fontSize: 13, fontWeight: '600', letterSpacing: 0.2 }, accentBorder: { borderLeftWidth: 3, borderTopLeftRadius: 12, borderBottomLeftRadius: 12, }, - cardValue: { - fontSize: 22, - fontWeight: '700', - letterSpacing: 0.35, - }, - cardSubtext: { - fontSize: 15, - fontWeight: '400', - marginTop: 4, - }, - placeholderBadge: { - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 6, - marginLeft: 'auto', - }, - placeholderBadgeText: { - fontSize: 11, - fontWeight: '500', - }, - - // Study timeline - timelineRow: { - flexDirection: 'row', - alignItems: 'center', - }, - timelineItem: { - flex: 1, - }, - timelineLabel: { - fontSize: 13, - fontWeight: '400', - marginBottom: 2, - }, - timelineValue: { - fontSize: 17, - fontWeight: '600', - }, - timelineDivider: { - width: StyleSheet.hairlineWidth, - height: 32, - marginHorizontal: 16, - }, - - // Watch reminder - reminderCard: { - borderRadius: 12, - padding: 16, - marginBottom: 12, - }, - reminderContent: { - flexDirection: 'row', - alignItems: 'flex-start', - gap: 12, - }, - reminderTextContainer: { - flex: 1, - }, - reminderTitle: { - fontSize: 15, - fontWeight: '600', - marginBottom: 2, - }, - reminderBody: { - fontSize: 13, - lineHeight: 18, - }, - - // All set + cardValue: { fontSize: 22, fontWeight: '700', letterSpacing: 0.35 }, + cardSubtext: { fontSize: 15, fontWeight: '400', marginTop: 4 }, + placeholderBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, marginLeft: 'auto' }, + placeholderBadgeText: { fontSize: 11, fontWeight: '500' }, + timelineRow: { flexDirection: 'row', alignItems: 'center' }, + timelineItem: { flex: 1 }, + timelineLabel: { fontSize: 13, fontWeight: '400', marginBottom: 2 }, + timelineValue: { fontSize: 17, fontWeight: '600' }, + timelineDivider: { width: StyleSheet.hairlineWidth, height: 32, marginHorizontal: 16 }, + reminderCard: { borderRadius: 12, padding: 16, marginBottom: 12 }, + reminderContent: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 }, + reminderTextContainer: { flex: 1 }, + reminderTitle: { fontSize: 15, fontWeight: '600', marginBottom: 2 }, + reminderBody: { fontSize: 13, lineHeight: 18 }, allSetCard: { flexDirection: 'row', alignItems: 'center', @@ -465,34 +1032,11 @@ const styles = StyleSheet.create({ padding: 16, marginBottom: 12, }, - allSetText: { - fontSize: 15, - fontWeight: '400', - }, - - // Study section - sectionTitle: { - fontSize: 17, - fontWeight: '600', - marginBottom: 6, - }, - studyBody: { - fontSize: 15, - lineHeight: 22, - }, + allSetText: { fontSize: 15, fontWeight: '400' }, - // Dev tools sheet - sheetOverlay: { - flex: 1, - justifyContent: 'flex-end', - backgroundColor: 'rgba(0,0,0,0.3)', - }, - sheetContent: { - borderTopLeftRadius: 14, - borderTopRightRadius: 14, - padding: 20, - paddingBottom: 40, - }, + // Dev sheet + sheetOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.3)' }, + sheetContent: { borderTopLeftRadius: 14, borderTopRightRadius: 14, padding: 20, paddingBottom: 40 }, sheetHandle: { width: 36, height: 5, @@ -516,17 +1060,7 @@ const styles = StyleSheet.create({ borderRadius: 10, marginBottom: 8, }, - sheetButtonText: { - fontSize: 17, - fontWeight: '400', - }, - sheetCancel: { - alignItems: 'center', - padding: 14, - marginTop: 4, - }, - sheetCancelText: { - fontSize: 17, - fontWeight: '600', - }, + sheetButtonText: { fontSize: 17, fontWeight: '400' }, + sheetCancel: { alignItems: 'center', padding: 14, marginTop: 4 }, + sheetCancelText: { fontSize: 17, fontWeight: '600' }, }); From 12abcb361f9fa27a26dc6e3ae59b3994daa5d801 Mon Sep 17 00:00:00 2001 From: James Rhee Date: Mon, 9 Mar 2026 17:42:14 -0700 Subject: [PATCH 2/2] Fix lint errors in home screen - Escape apostrophe in JSX text (react/no-unescaped-entities) - Remove unused imports: useAuth, computeSummaryStats - Remove unused variables: isDark, user, outerCenter Co-Authored-By: Claude Sonnet 4.6 --- homeflow/app/(tabs)/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeflow/app/(tabs)/index.tsx b/homeflow/app/(tabs)/index.tsx index 0ae6f5a..89b38be 100644 --- a/homeflow/app/(tabs)/index.tsx +++ b/homeflow/app/(tabs)/index.tsx @@ -32,7 +32,6 @@ import { useSurgeryDate } from '@/hooks/use-surgery-date'; import { useWatchUsage } from '@/hooks/use-watch-usage'; import { SurgeryCompleteModal } from '@/components/home/SurgeryCompleteModal'; import { useAppTheme } from '@/lib/theme/ThemeContext'; -import { useAuth } from '@/lib/auth/auth-context'; import { useHealthSummary } from '@/hooks/use-health-summary'; import { fetchSessions, @@ -47,7 +46,6 @@ import { import { filterByRange, bucketSeries, - computeSummaryStats, type BucketPoint, } from '@/src/data/voidingAggregation'; import { @@ -186,7 +184,6 @@ function ActivityRings({ const MIDDLE = OUTER - STROKE * 2 - GAP * 2; const INNER = MIDDLE - STROKE * 2 - GAP * 2; - const outerCenter = OUTER / 2; const middleOffset = STROKE + GAP; const innerOffset = STROKE * 2 + GAP * 2; @@ -415,8 +412,7 @@ function daysUntil(dateStr: string): string { export default function HomeScreen() { const { theme } = useAppTheme(); - const { isDark, colors: t } = theme; - const { user } = useAuth(); + const { colors: t } = theme; const surgery = useSurgeryDate(); const watch = useWatchUsage(); @@ -647,7 +643,7 @@ export default function HomeScreen() { Apple Health - Today's summary + {"Today's summary"}