frontend: anchor the sleep history card on the picked day
The sleep card derived its timestamp from the creation instant, so a record logged today for an earlier day showed up under today in the feed. Sleep has no clock anchor by design: an estimate for a day, with extra same-day entries read as naps, so the card now anchors on the day the user picked, matching the API's own ordering and the insights. The clock time, meaningless without a real anchor, is gone
Pedro Lucas Porcellis porcellis@eletrotupi.com · 9 days ago
c121404b00d29de6d91827ce3a3fc0ede1a0c5ab Browse filesParents
a1a9079Changed files
3
Additions
+27
Deletions
-19
frontend/components/history/SleepRecordCard.tsx
1 1
import { View, StyleSheet } from 'react-native'
2 2
import { Text } from '@/components/ui/Text'
3 3
import { MaterialIcons } from '@expo/vector-icons'
4 4
import { Card } from '@/components/ui/Cards'
5 5
import { Badge } from '@/components/ui/Badge'
28 28
29 29
export function SleepRecordCard({ card }: Props) {
30 30
const badge = sleepBadge(card.raw.average)
31 31
const duration = formatDuration(card.raw.average)
32 32
33 -
const time = new Date(card.timestamp)
34 -
.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
35 -
36 33
return (
37 34
<Card style={styles.card} onPress={() => router.push(`/sleep/${card.raw.id}`)}>
38 35
<View style={styles.row}>
39 36
<View style={[styles.iconWrap, { backgroundColor: '#ede9fe' }]}>
40 37
<MaterialIcons name="bedtime" size={22} color="#7c3aed" />
41 38
</View>
42 39
<View style={styles.body}>
43 40
<View style={styles.titleRow}>
44 41
<Text style={styles.title}>Sono</Text>
45 -
<Text style={styles.time}>{time}</Text>
46 42
</View>
47 43
<View style={styles.durationRow}>
48 44
<Text style={styles.duration}>{duration}</Text>
49 45
</View>
50 46
{card.raw.annotations ? (
86 82
marginBottom: 4
87 83
},
88 84
title: {
89 85
fontSize: 15,
90 86
fontWeight: '600'
91 -
},
92 -
time: {
93 -
fontSize: 13,
94 -
opacity: 0.45
95 87
},
96 88
durationRow: {
97 89
flexDirection: 'row',
98 90
alignItems: 'center',
99 91
gap: 8,
115 107
alignSelf: 'flex-start',
116 108
marginTop: 4,
117 109
maxWidth: '50%'
118 110
},
119 111
})
frontend/lib/history/mappers.ts
1 1
import type { HistoryCard } from "@/lib/history/types";
2 2
import type { MoodEntry, SleepRecord, Trigger, CareAction } from "@/lib/api";
3 3
import { getMood } from "@/constants/moods";
4 +
import { parseDateOnly } from "@/lib/utils/time";
4 5
5 6
6 7
export function mapMoodToHistoryCard(mood: MoodEntry): HistoryCard {
7 8
const moodLabel = getMood(mood.selectedMood);
8 9
const components = mood.moodComponents
39 40
export function mapSleepToHistoryCard(record: SleepRecord): HistoryCard {
40 41
const totalMinutes = Math.round(record.average * 60);
41 42
const hours = Math.floor(totalMinutes / 60);
42 43
const minutes = totalMinutes % 60;
43 44
const duration = `${hours}h ${minutes.toString().padStart(2, "0")}min`;
44 -
const timestamp = typeof record.createdAt === 'string'
45 -
? record.createdAt
46 -
: record.createdAt.toISOString();
47 45
48 46
return {
49 47
id: `sleep-${record.id}`,
50 48
category: "sleep",
51 -
timestamp,
49 +
timestamp: parseDateOnly(record.date).toISOString(),
52 50
title: "Sono",
53 51
summary: [duration, record.annotations].filter(Boolean).join(" · "),
54 52
badge: sleepQualityBadge(record.average),
55 53
raw: record,
56 54
};
160 158
summary: "",
161 159
raw: record,
162 160
};
163 161
}
164 162
}
frontend/tests/history/mappers.unit.test.ts
1 1
import {
2 2
mapCareActionToHistoryCard,
3 3
mapMoodToHistoryCard,
4 4
mapSleepToHistoryCard,
5 5
mapTriggerToHistoryCard,
11 11
MoodEntry,
12 12
SleepRecord,
13 13
Trigger,
14 14
} from "@/lib/api";
15 15
import type { MoodComponent } from "@/lib/api/types";
16 +
import { parseDateOnly } from "@/lib/utils/time";
16 17
17 18
const FIXED = new Date(2025, 0, 15, 10, 0, 0);
18 19
19 20
function mood(overrides: Partial<MoodEntry> = {}): MoodEntry {
20 21
return {
150 151
it("maps the record into a sleep card", () => {
151 152
const record = sleep({
152 153
id: 4,
153 154
average: 7.5,
154 155
annotations: "Dormi bem",
155 -
createdAt: new Date(2025, 0, 15, 8, 0, 0),
156 +
date: new Date(Date.UTC(2025, 0, 15)),
157 +
createdAt: new Date(2025, 0, 20, 8, 0, 0),
156 158
});
157 159
158 160
expect(mapSleepToHistoryCard(record)).toStrictEqual({
159 161
id: "sleep-4",
160 162
category: "sleep",
161 -
timestamp: record.createdAt.toISOString(),
163 +
timestamp: parseDateOnly(record.date).toISOString(),
162 164
title: "Sono",
163 165
summary: "7h 30min · Dormi bem",
164 166
badge: { label: "Excelente", variant: "green" },
165 167
raw: record,
166 168
});
167 169
});
168 170
169 -
it("keeps a string timestamp as-is", () => {
170 -
const timestamp = "2025-01-15T13:00:00.000Z";
171 -
const card = mapSleepToHistoryCard(
172 -
sleep({ createdAt: timestamp as unknown as Date }),
171 +
it("anchors on the picked day, not the creation instant", () => {
172 +
const record = sleep({
173 +
date: new Date(Date.UTC(2025, 0, 10)),
174 +
createdAt: new Date(2025, 0, 15, 8, 0, 0),
175 +
});
176 +
177 +
const card = mapSleepToHistoryCard(record);
178 +
179 +
expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
180 +
"10/01/2025",
173 181
);
182 +
});
174 183
175 -
expect(card.timestamp).toBe(timestamp);
184 +
it("reads the date-only string the API sends", () => {
185 +
const record = sleep({
186 +
date: "2025-01-10T00:00:00.000Z" as unknown as Date,
187 +
});
188 +
189 +
const card = mapSleepToHistoryCard(record);
190 +
191 +
expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
192 +
"10/01/2025",
193 +
);
176 194
});
177 195
178 196
it("omits the separator when there is no annotation", () => {
179 197
expect(mapSleepToHistoryCard(sleep({ average: 8 })).summary).toBe(
180 198
"8h 00min",
441 459
summary: "",
442 460
raw: record,
443 461
});
444 462
});
445 463
});