frontend: new page to display a single sleep record entry (wip)
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
24f49f7fa94d199c1c1ba1f8424a195cd7e6ee9d Browse filesParents
2e52778Changed files
3
Additions
+291
Deletions
-2
/dev/null → frontend/app/sleep/[id].tsx
1 +
import { useState, useEffect, use } from "react";
2 +
import {
3 +
StyleSheet,
4 +
Text,
5 +
View,
6 +
ScrollView,
7 +
Pressable,
8 +
ActivityIndicator,
9 +
} from "react-native";
10 +
import { useLocalSearchParams, router, Stack } from "expo-router";
11 +
import { format } from "date-fns";
12 +
import { ptBR } from "date-fns/locale";
13 +
14 +
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15 +
import { Badge } from "@/components/ui/Badge";
16 +
import { Button } from "@/components/ui/Button";
17 +
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
18 +
import { Section, SectionHeader } from "@/components/ui/Sections";
19 +
import { Theme, Colors, Spacing } from "@/constants/theme";
20 +
import { MaterialIcons } from "@expo/vector-icons";
21 +
import {
22 +
useSleepRecord,
23 +
useDeleteSleepRecord,
24 +
} from "@/hooks/useSleepRecord.queries";
25 +
import { formatDate } from "@/lib/utils/time";
26 +
27 +
export default function SleepDetailScreen() {
28 +
const { id } = useLocalSearchParams<{ id: string }>();
29 +
const { data: sleepRecord, isLoading, isError } = useSleepRecord(id);
30 +
const deleteSleepRecord = useDeleteSleepRecord();
31 +
const [minutesLeft, setMinutesLeft] = useState(0);
32 +
const [canDelete, setCanDelete] = useState(false);
33 +
34 +
useEffect(() => {
35 +
const updateTimer = () => {
36 +
const createdAt = sleepRecord?.createdAt;
37 +
if (!createdAt) return;
38 +
39 +
const msSinceCreation = Date.now() - createdAt.getTime();
40 +
const deletionWindowMs = 5 * 60 * 1000;
41 +
42 +
const newCanDelete = msSinceCreation < deletionWindowMs;
43 +
const newMinutesLeft = Math.max(
44 +
0,
45 +
Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
46 +
);
47 +
48 +
setCanDelete(newCanDelete);
49 +
setMinutesLeft(newMinutesLeft);
50 +
};
51 +
52 +
if (isLoading) return;
53 +
54 +
updateTimer(); // Set initial values
55 +
56 +
const interval = setInterval(updateTimer, 1000);
57 +
58 +
return () => clearInterval(interval);
59 +
}, [sleepRecord, isLoading]);
60 +
61 +
console.log("sleepRecord", sleepRecord)
62 +
console.log("isLoading", isLoading)
63 +
64 +
if (isLoading) {
65 +
return (
66 +
<View style={styles.centered}>
67 +
<ActivityIndicator size="large" color={Colors.light.tint} />
68 +
</View>
69 +
);
70 +
}
71 +
72 +
if (isError || !sleepRecord) {
73 +
return (
74 +
<View style={styles.centered}>
75 +
<Text style={styles.errorText}>Registro não encontrado.</Text>
76 +
<Pressable onPress={() => router.back()} style={styles.backButton}>
77 +
<Text style={styles.backButtonText}>Voltar</Text>
78 +
</Pressable>
79 +
</View>
80 +
);
81 +
}
82 +
83 +
const handleDelete = async () => {
84 +
await deleteSleepRecord.mutateAsync(String(sleepRecord.id));
85 +
86 +
router.back();
87 +
};
88 +
89 +
const deleteButtonMsg = () => {
90 +
const msg = "Excluir Registro";
91 +
const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
92 +
const suffix = canDelete ? timeLeftStr : "(Expirado)";
93 +
94 +
return `${msg} ${suffix}`;
95 +
};
96 +
97 +
return (
98 +
<>
99 +
<Stack.Screen options={{ title: "Ver detalhes" }} />
100 +
101 +
<ScrollView
102 +
style={styles.container}
103 +
contentContainerStyle={styles.content}
104 +
showsVerticalScrollIndicator={false}
105 +
>
106 +
{/* Header card */}
107 +
<Card style={styles.headerCard}>
108 +
<Row style={{ alignItems: "center" }}>
109 +
<View style={[styles.iconWrap, { backgroundColor: '#ede9fe' }]}>
110 +
<MaterialIcons name="bedtime" size={22} color="#7c3aed" />
111 +
</View>
112 +
<Col>
113 +
<Row style={{ alignItems: "flex-end" }}>
114 +
<Text style={styles.averageLabel}>{sleepRecord.average}h</Text>
115 +
<Text style={styles.suffixLabel}>de sono</Text>
116 +
</Row>
117 +
118 +
<Text style={styles.dateLabel}>
119 +
{formatDate(new Date(sleepRecord.date))}
120 +
</Text>
121 +
</Col>
122 +
</Row>
123 +
</Card>
124 +
125 +
{/* Annotation / notes */}
126 +
{sleepRecord.annotations && (
127 +
<Section>
128 +
<SectionHeader title="Anotações" variant="subtle" />
129 +
<Card style={{ padding: Spacing.cardGap }}>
130 +
<Text style={styles.annotation}>
131 +
"{sleepRecord.annotations?.trim()}"
132 +
</Text>
133 +
</Card>
134 +
</Section>
135 +
)}
136 +
137 +
{/* Delete */}
138 +
<View style={styles.section}>
139 +
<Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
140 +
<Button
141 +
variant="danger"
142 +
isLoading={deleteSleepRecord.isPending}
143 +
disabled={!canDelete || deleteSleepRecord.isPending}
144 +
onPress={handleDelete}
145 +
title={deleteButtonMsg()}
146 +
/>
147 +
148 +
<SubtleInfoCard
149 +
style={styles.deleteInfo}
150 +
text={`
151 +
A exclusão de registros é permitida apenas nos primeiros 5 minutos
152 +
após o envio para garantir a integridade do histórico emocional.
153 +
`}
154 +
/>
155 +
</View>
156 +
</ScrollView>
157 +
</>
158 +
);
159 +
}
160 +
161 +
const styles = StyleSheet.create({
162 +
container: {
163 +
flex: 1,
164 +
backgroundColor: Theme.colors.background,
165 +
},
166 +
content: {
167 +
padding: Spacing.containerPadding,
168 +
gap: Spacing.sectionGap,
169 +
paddingBottom: 48,
170 +
},
171 +
iconWrap: {
172 +
width: 40,
173 +
height: 40,
174 +
borderRadius: 20,
175 +
alignItems: 'center',
176 +
justifyContent: 'center',
177 +
marginTop: 1
178 +
},
179 +
headerCardContent: {
180 +
flexDirection: "row",
181 +
alignItems: "center",
182 +
gap: 8,
183 +
},
184 +
averageLabel: {
185 +
height: '100%',
186 +
fontSize: 24,
187 +
fontWeight: "700",
188 +
color: Colors.light.text,
189 +
},
190 +
averageLabelSuffix: {
191 +
height: '100%',
192 +
fontSize: 17,
193 +
fontWeight: "700",
194 +
color: Colors.light.textSecondary,
195 +
},
196 +
centered: {
197 +
flex: 1,
198 +
justifyContent: "center",
199 +
alignItems: "center",
200 +
gap: 12,
201 +
},
202 +
errorText: {
203 +
fontSize: 15,
204 +
color: Colors.light.textSecondary,
205 +
},
206 +
backButton: {
207 +
paddingHorizontal: 20,
208 +
paddingVertical: 10,
209 +
backgroundColor: Theme.colors.tint,
210 +
borderRadius: Theme.borderRadius.md,
211 +
},
212 +
backButtonText: {
213 +
fontWeight: "700",
214 +
color: Theme.colors.text,
215 +
},
216 +
217 +
// Header
218 +
headerCard: {
219 +
padding: 16,
220 +
},
221 +
moodIcon: {
222 +
fontSize: 40,
223 +
},
224 +
moodLabel: {
225 +
fontSize: 17,
226 +
fontWeight: "700",
227 +
color: Colors.light.text,
228 +
},
229 +
moodTime: {
230 +
fontSize: 13,
231 +
color: Colors.light.textSecondary,
232 +
},
233 +
234 +
// Sections
235 +
section: {
236 +
gap: 8,
237 +
},
238 +
sectionLabel: {
239 +
fontSize: 11,
240 +
fontWeight: "600",
241 +
letterSpacing: 0.8,
242 +
color: Colors.light.textSecondary,
243 +
textTransform: "uppercase",
244 +
},
245 +
246 +
// Components
247 +
componentRow: {
248 +
flexDirection: "row",
249 +
justifyContent: "space-between",
250 +
alignItems: "center",
251 +
paddingVertical: 10,
252 +
},
253 +
componentLabel: {
254 +
fontSize: 15,
255 +
fontWeight: "700",
256 +
color: Colors.light.text,
257 +
},
258 +
componentIntensity: {
259 +
fontSize: 13,
260 +
color: Colors.light.textSecondary,
261 +
},
262 +
componentIntensityGreen: {
263 +
fontSize: 13,
264 +
fontWeight: "700",
265 +
color: Theme.colors.tint,
266 +
},
267 +
progressTrack: {
268 +
height: 6,
269 +
backgroundColor: Theme.colors.light.divider,
270 +
borderRadius: Theme.borderRadius.full,
271 +
marginBottom: 4,
272 +
overflow: "hidden",
273 +
},
274 +
progressFill: {
275 +
height: "100%",
276 +
backgroundColor: Theme.colors.light.tint,
277 +
borderRadius: Theme.borderRadius.full,
278 +
},
279 +
// Annotation
280 +
annotation: {
281 +
fontSize: 14,
282 +
lineHeight: 22,
283 +
color: Colors.light.textSecondary,
284 +
fontStyle: "italic",
285 +
padding: 4,
286 +
},
287 +
});
frontend/components/history/MoodHistoryCard.tsx
1 1
// components/history/cards/MoodHistoryCard.tsx
2 2
import { View, Text, StyleSheet } from 'react-native';
3 3
import { MaterialIcons } from '@expo/vector-icons';
4 4
import { Card } from '@/components/ui/Cards';
5 5
import { Badge } from '@/components/ui/Badge';
6 6
import type { HistoryCard } from '@/lib/history/types';
7 7
import type { MoodEntry } from '@/lib/api';
8 8
import { Spacing } from '@/constants/theme';
9 9
import { getMood } from '@/constants/moods';
10 10
import { format } from 'date-fns';
11 +
import { router } from 'expo-router';
11 12
12 13
function inferBadge(anxiety: number, stress: number, energy: number) {
13 14
if (anxiety >= 7) return { label: 'Ansiedade alta', variant: 'red' } as const
14 15
if (stress >= 7) return { label: 'Estresse alto', variant: 'orange' } as const
15 16
if (energy <= 3) return { label: 'Energia baixa', variant: 'yellow' } as const
33 34
const badge = inferBadge(card.raw.anxietyLevel, card.raw.stressLevel, card.raw.energyLevel)
34 35
const time = format(card.timestamp, 'HH:mm')
35 36
const moodDef = getMood(card.raw.selectedMood.toLowerCase());
36 37
37 38
return (
38 -
<Card style={styles.card}>
39 +
<Card style={styles.card} onPress={() => router.push(`/entry/${card.raw.id}`)}>
39 40
<View style={styles.row}>
40 41
<View style={[styles.iconWrap, { backgroundColor: meta.bg }]}>
41 42
<Text style={styles.moodIconEmoji}>{moodDef?.icon ?? '😐'}</Text>
42 43
43 44
{false && (<MaterialIcons name={meta.icon} size={22} color={meta.color} />)}
105 106
alignSelf: 'flex-start',
106 107
marginTop: 4,
107 108
maxWidth: '50%'
108 109
}
109 110
})
frontend/components/history/SleepRecordCard.tsx
1 1
import { View, Text, StyleSheet } from 'react-native'
2 2
import { MaterialIcons } from '@expo/vector-icons'
3 3
import { Card } from '@/components/ui/Cards'
4 4
import { Badge } from '@/components/ui/Badge'
5 5
import type { SleepRecord } from '@/lib/api'
6 6
import { Spacing } from '@/constants/theme';
7 +
import { router } from 'expo-router'
7 8
8 9
function sleepBadge(hours: number) {
9 10
if (hours >= 7) return { label: 'Excelente', variant: 'green' } as const
10 11
if (hours >= 5) return { label: 'Regular', variant: 'yellow' } as const
11 12
29 30
30 31
const time = new Date(card.timestamp)
31 32
.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
32 33
33 34
return (
34 -
<Card style={styles.card}>
35 +
<Card style={styles.card} onPress={() => router.push(`/sleep/${card.raw.id}`)}>
35 36
<View style={styles.row}>
36 37
<View style={[styles.iconWrap, { backgroundColor: '#ede9fe' }]}>
37 38
<MaterialIcons name="bedtime" size={22} color="#7c3aed" />
38 39
</View>
39 40
<View style={styles.body}>
112 113
alignSelf: 'flex-start',
113 114
marginTop: 4,
114 115
maxWidth: '50%'
115 116
},
116 117
})