eletrotupi / tcc/ commit / 24f49f7

frontend: new page to display a single sleep record entry (wip)

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 24f49f7fa94d199c1c1ba1f8424a195cd7e6ee9d Browse files
Parents 2e52778
Changed 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
})