frontend/insights: the daily pair reads today's insight
The insight hook now returns metadata typed for the type requested, so the energy and sleep cards read their comparison and window fields with no cast. The cache key picks up the device's local date, so the pair refetches after midnight rather than keeping yesterday's row
Pedro Lucas Porcellis porcellis@eletrotupi.com · 12 days ago
8873ae72bb5c5381f95356155b0c0cb1d4041156 Browse filesParents
b36ca93Changed files
5
Additions
+66
Deletions
-61
frontend/components/ui/DailyEnergyWidget.tsx
1 1
import { ActivityIndicator, StyleSheet, View } from "react-native";
2 2
import { Text } from "@/components/ui/Text";
3 3
import { Card } from "@/components/ui/Cards";
4 4
import { useInsight } from "@/hooks/useInsights.queries";
5 5
import { Ionicons } from "@expo/vector-icons";
6 6
import { Typography, Spacing, Colors, BorderRadius } from "@/constants/theme";
7 7
8 8
export const DailyEnergyWidget = () => {
9 9
const { data: insight, isLoading } = useInsight("DAILY_ENERGY");
10 10
11 -
const metadata = insight?.metadata as {
12 -
peak: number;
13 -
trough: number;
14 -
entryCount: number;
15 -
avgEnergy: number;
16 -
};
11 +
const metadata = insight?.metadata;
17 12
18 -
const avgEnergy = metadata?.avgEnergy;
19 -
const peak = metadata?.peak;
13 +
const avgEnergy = metadata?.avgEnergy ?? 0;
14 +
const peak = metadata?.peak ?? 0;
20 15
21 -
const energyWidth = (avgEnergy / peak) * 100;
16 +
const energyWidth = peak > 0 ? (avgEnergy / peak) * 100 : 0;
22 17
const energySummary = () => {
23 18
if (avgEnergy < 3) return "Energia baixa";
24 19
if (avgEnergy < 5) return "Media baixa";
25 20
if (avgEnergy < 7.5) return "Média Alta";
26 21
return "Energia alta";
96 91
fontSize: 11,
97 92
fontWeight: 600,
98 93
lineHeight: 16.5,
99 94
},
100 95
});
frontend/components/ui/DailySleepWidget.tsx
1 1
import React from "react";
2 2
import { View, StyleSheet, ActivityIndicator } from "react-native";
3 3
import { Text } from "@/components/ui/Text";
4 4
import { Card } from "./Cards";
5 5
import { Colors, Spacing, Typography } from "@/constants/theme";
7 7
import { useInsight } from "@/hooks/useInsights.queries";
8 8
9 9
export const DailySleepWidget = () => {
10 10
const { data: insight, isLoading } = useInsight("DAILY_SLEEP");
11 11
12 -
const metadata = insight?.metadata as
13 -
| {
14 -
diffStr: string;
15 -
diffMinutes: number;
16 -
}
17 -
| undefined;
12 +
const metadata = insight?.metadata;
18 13
19 14
const diffColor = metadata?.diffMinutes
20 15
? metadata.diffMinutes < 0
21 16
? Colors.light.danger
22 17
: Colors.light.tint
76 71
empty: {
77 72
...Typography.labelSm,
78 73
opacity: 0.5,
79 74
},
80 75
});
frontend/hooks/useInsights.queries.ts
1 +
import { format } from "date-fns";
1 2
import { useQuery } from "@tanstack/react-query";
2 3
import { apiClient, Insight, InsightType, InsightPeriod } from "@/lib/api";
4 +
5 +
// The device's local calendar day, used only to roll the cache over at midnight
6 +
// so the pair refetches; the server still resolves the actual day boundary
7 +
function localDay(): string {
8 +
return format(new Date(), "yyyy-MM-dd");
9 +
}
3 10
4 11
export const insightKeys = {
5 12
all: () => ["insights"] as const,
6 13
lists: () => [...insightKeys.all(), "list"] as const,
7 14
list: (filters?: InsightFilters) =>
8 15
[...insightKeys.lists(), filters] as const,
9 -
byType: (type: InsightType) => [...insightKeys.all(), "type", type] as const,
16 +
byType: (type: InsightType) =>
17 +
[...insightKeys.all(), "type", type, localDay()] as const,
10 18
};
11 19
12 20
interface InsightFilters {
13 21
type?: InsightType;
14 22
period?: InsightPeriod;
27 35
ENERGY_SLEEP_CORRELATION: "WEEKLY",
28 36
TRIGGER_PATTERN: "WEEKLY",
29 37
DAILY_SLEEP: "DAILY",
30 38
};
31 39
32 -
export const useInsight = (type: InsightType) => {
40 +
export const useInsight = <T extends InsightType>(type: T) => {
33 41
const period = INSIGHT_PERIOD[type];
34 42
const staleTime = INSIGHT_STALE_TIMES[period];
35 43
36 44
return useQuery({
37 45
queryKey: insightKeys.byType(type),
38 46
queryFn: () =>
39 47
apiClient
40 -
.getInsights({ type, limit: 1 })
48 +
.getInsights<T>({ type, limit: 1 })
41 49
.then((res) => res.insights[0] ?? null),
42 50
staleTime,
43 51
});
44 52
};
45 53
48 56
queryKey: insightKeys.list(filters),
49 57
queryFn: () => apiClient.getInsights(filters).then((res) => res.insights),
50 58
staleTime: 1000 * 60 * 15,
51 59
});
52 60
};
frontend/lib/api/client.ts
1 1
import * as SecureStore from "expo-secure-store";
2 2
import Constants from "expo-constants";
3 3
4 4
export class ApiError extends Error {
5 5
status: number;
362 362
async deleteTrigger(id: string): Promise<void> {
363 363
return this.request(`/triggers/${id}`, { method: "DELETE" });
364 364
}
365 365
366 366
// Insights
367 -
async getInsights(filters?: {
368 -
type?: InsightType;
367 +
async getInsights<T extends InsightType = InsightType>(filters?: {
368 +
type?: T;
369 369
period?: InsightPeriod;
370 370
limit?: number;
371 -
}): Promise<{ insights: Insight[] }> {
371 +
}): Promise<{ insights: Insight<T>[] }> {
372 372
const query = new URLSearchParams();
373 373
if (filters?.type) query.set("type", filters.type);
374 374
if (filters?.period) query.set("period", filters.period);
375 375
if (filters?.limit) query.set("limit", String(filters.limit));
376 376
377 377
const qs = query.toString();
378 -
return this.request(`/insights${qs ? `?${qs}` : ""}`);
378 +
return this.request<{ insights: Insight<T>[] }>(
379 +
`/insights${qs ? `?${qs}` : ""}`,
380 +
);
379 381
}
380 382
381 383
// Medicine Regimens
382 384
async getMedicineRegimens(): Promise<{ regimens: MedicineRegimen[] }> {
383 385
return this.request('/medicine-regimens');
525 527
return response.arrayBuffer()
526 528
}
527 529
}
528 530
529 531
export const apiClient = new ApiClient();
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
167 167
| "DAILY_ENERGY"
168 168
| "DAILY_SLEEP";
169 169
170 170
export type InsightPeriod = "DAILY" | "WEEKLY" | "MONTHLY";
171 171
172 -
export type InsightMetadata =
173 -
| {
174 -
type: "MOOD_TREND";
175 -
delta: number;
176 -
avgFirst: number;
177 -
avgSecond: number;
178 -
dominantMood: string;
179 -
avgEnergy: number;
180 -
}
181 -
| {
182 -
type: "ENERGY_SLEEP_CORRELATION";
183 -
correlationScore: number;
184 -
sampleSize: number;
185 -
}
186 -
| {
187 -
type: "TRIGGER_PATTERN";
188 -
topCategory: string;
189 -
topCount: number;
190 -
total: number;
191 -
distribution: Record<string, number>;
192 -
}
193 -
| {
194 -
type: "DAILY_ENERGY";
195 -
avgEnergy: number;
196 -
dayTrend: number | null;
197 -
peak: number;
198 -
trough: number;
199 -
entryCount: number;
200 -
}
201 -
| {
202 -
type: "DAILY_SLEEP";
203 -
totalHours: number;
204 -
diffMinutes: number;
205 -
sessionCount: number;
206 -
hadNaps: boolean;
207 -
};
172 +
export interface InsightMetadataByType {
173 +
MOOD_TREND: {
174 +
delta: number;
175 +
avgFirst: number;
176 +
avgSecond: number;
177 +
dominantMood: string;
178 +
avgEnergy: number;
179 +
};
180 +
ENERGY_SLEEP_CORRELATION: {
181 +
correlationScore: number;
182 +
sampleSize: number;
183 +
};
184 +
TRIGGER_PATTERN: {
185 +
topCategory: string;
186 +
topCount: number;
187 +
total: number;
188 +
distribution: Record<string, number>;
189 +
};
190 +
DAILY_ENERGY: {
191 +
avgEnergy: number;
192 +
dayTrend: number | null;
193 +
peak: number;
194 +
trough: number;
195 +
entryCount: number;
196 +
yesterdayAvg: number | null;
197 +
diffEnergy: number | null;
198 +
windowLabel: string;
199 +
};
200 +
DAILY_SLEEP: {
201 +
totalHours: number;
202 +
diffMinutes: number | null;
203 +
diffStr: string;
204 +
sessionCount: number;
205 +
napStr: string;
206 +
hadNaps: boolean;
207 +
windowLabel: string;
208 +
};
209 +
}
210 +
211 +
export type InsightMetadata<T extends InsightType = InsightType> =
212 +
InsightMetadataByType[T];
208 213
209 -
export type Insight = {
214 +
export type Insight<T extends InsightType = InsightType> = {
210 215
id: number;
211 -
type: InsightType;
216 +
type: T;
212 217
period: InsightPeriod;
213 218
title: string;
214 219
body: string;
215 -
metadata: InsightMetadata;
220 +
metadata: InsightMetadata<T>;
216 221
generatedAt: string;
217 222
periodStart: string;
218 223
periodEnd: string;
219 224
};
220 225
389 394
moodId?: number;
390 395
}
391 396
export interface MedicineRegimenResponse { regimen: MedicineRegimen; }
392 397
393 398
export interface LinkMoodPayload { moodId: number; perceivedImpact: number; }