frontend: reorganizing triggers' metadata
Parents:
e2b786d4 file(s) changed
- frontend/app/triggers/[id].tsx +240 -0
- frontend/app/triggers/new.tsx +7 -27
- frontend/components/history/TriggerCard.tsx +6 -17
- frontend/constants/trigger-categories.ts +24 -0
frontend/app/triggers/[id].tsx
@@ -0,0 +1,240 @@
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, Typography, Colors, Spacing } from "@/constants/theme";
20 + import { MaterialIcons } from "@expo/vector-icons";
21 + import { getTriggerCategory } from "@/constants/trigger-categories";
22 + import {
23 + useTrigger,
24 + useDeleteTrigger,
25 + } from "@/hooks/useTrigger.queries";
26 + import { formatMoment } from "@/lib/utils/time";
27 +
28 + export default function TriggerDetailsScreen() {
29 + const { id } = useLocalSearchParams<{ id: string }>();
30 + const { data: trigger, isLoading, isError } = useTrigger(id);
31 + const deleteTrigger = useDeleteTrigger();
32 + const [minutesLeft, setMinutesLeft] = useState(0);
33 + const [canDelete, setCanDelete] = useState(false);
34 +
35 + useEffect(() => {
36 + const updateTimer = () => {
37 + const createdAt = new Date(trigger?.createdAt);
38 + if (!createdAt) return;
39 +
40 + const msSinceCreation = Date.now() - createdAt.getTime();
41 + const deletionWindowMs = 5 * 60 * 1000;
42 +
43 + const newCanDelete = msSinceCreation < deletionWindowMs;
44 + const newMinutesLeft = Math.max(
45 + 0,
46 + Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
47 + );
48 +
49 + setCanDelete(newCanDelete);
50 + setMinutesLeft(newMinutesLeft);
51 + };
52 +
53 + if (isLoading) return;
54 +
55 + updateTimer(); // Set initial values
56 +
57 + const interval = setInterval(updateTimer, 1000);
58 +
59 + return () => clearInterval(interval);
60 + }, [trigger, isLoading]);
61 +
62 + console.log("trigger", trigger)
63 + console.log("isLoading", isLoading)
64 +
65 + if (isLoading) {
66 + return (
67 + <View style={styles.centered}>
68 + <ActivityIndicator size="large" color={Colors.light.tint} />
69 + </View>
70 + );
71 + }
72 +
73 + if (isError || !trigger) {
74 + return (
75 + <View style={styles.centered}>
76 + <Text style={styles.errorText}>Registro não encontrado.</Text>
77 + <Pressable onPress={() => router.back()} style={styles.backButton}>
78 + <Text style={styles.backButtonText}>Voltar</Text>
79 + </Pressable>
80 + </View>
81 + );
82 + }
83 +
84 + const meta = getTriggerCategory(trigger.category);
85 +
86 + const handleDelete = async () => {
87 + await deleteTrigger.mutateAsync(String(trigger.id));
88 +
89 + router.back();
90 + };
91 +
92 + const deleteButtonMsg = () => {
93 + const msg = "Excluir Registro";
94 + const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
95 + const suffix = canDelete ? timeLeftStr : "(Expirado)";
96 +
97 + return `${msg} ${suffix}`;
98 + };
99 +
100 + return (
101 + <>
102 + <Stack.Screen options={{ title: "Ver detalhes" }} />
103 +
104 + <ScrollView
105 + style={styles.container}
106 + contentContainerStyle={styles.content}
107 + showsVerticalScrollIndicator={false}
108 + >
109 + {/* Header card */}
110 + <Card style={styles.headerCard}>
111 + <Row style={{ alignItems: "center" }}>
112 + <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
113 + <MaterialIcons name={meta.icon} size={22} color={meta.color} />
114 + </View>
115 +
116 + <Col gap={2}>
117 + <Text style={styles.triggerLabel}>
118 + {meta.label}
119 + </Text>
120 + <Text style={styles.triggerMoment}>
121 + {formatMoment(new Date(trigger.moment))}
122 + </Text>
123 + </Col>
124 + </Row>
125 + </Card>
126 +
127 + {/* Annotation / notes */}
128 + {trigger.comment && (
129 + <Section>
130 + <SectionHeader title="Anotações" variant="subtle" />
131 + <Card style={{ padding: Spacing.cardGap }}>
132 + <Text style={styles.annotation}>
133 + "{trigger.comment?.trim()}"
134 + </Text>
135 + </Card>
136 + </Section>
137 + )}
138 +
139 + {/* Delete */}
140 + <View style={styles.section}>
141 + <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
142 + <Button
143 + variant="danger"
144 + isLoading={deleteTrigger.isPending}
145 + disabled={!canDelete || deleteTrigger.isPending}
146 + onPress={handleDelete}
147 + title={deleteButtonMsg()}
148 + />
149 +
150 + <SubtleInfoCard
151 + style={styles.deleteInfo}
152 + text={`
153 + A exclusão de registros é permitida apenas nos primeiros 5 minutos
154 + após o envio para garantir a integridade do histórico emocional.
155 + `}
156 + />
157 + </View>
158 + </ScrollView>
159 + </>
160 + );
161 + }
162 +
163 + const styles = StyleSheet.create({
164 + container: {
165 + flex: 1,
166 + backgroundColor: Theme.colors.background,
167 + },
168 + content: {
169 + padding: Spacing.containerPadding,
170 + gap: Spacing.sectionGap,
171 + paddingBottom: 48,
172 + },
173 + iconWrap: {
174 + width: 40,
175 + height: 40,
176 + borderRadius: 20,
177 + alignItems: 'center',
178 + justifyContent: 'center',
179 + marginTop: 1
180 + },
181 + headerCardContent: {
182 + flexDirection: "row",
183 + alignItems: "center",
184 + gap: 8,
185 + },
186 + triggerLabel: {
187 + ...Typography.headlineLg,
188 + color: Colors.light.text,
189 + },
190 + centered: {
191 + flex: 1,
192 + justifyContent: "center",
193 + alignItems: "center",
194 + gap: 12,
195 + },
196 + errorText: {
197 + fontSize: 15,
198 + color: Colors.light.textSecondary,
199 + },
200 + backButton: {
201 + paddingHorizontal: 20,
202 + paddingVertical: 10,
203 + backgroundColor: Theme.colors.tint,
204 + borderRadius: Theme.borderRadius.md,
205 + },
206 + backButtonText: {
207 + fontWeight: "700",
208 + color: Theme.colors.text,
209 + },
210 +
211 + // Header
212 + headerCard: {
213 + padding: 16,
214 + },
215 + triggerMoment: {
216 + fontSize: 13,
217 + color: Colors.light.textSecondary,
218 + },
219 +
220 + // Sections
221 + section: {
222 + gap: 8,
223 + },
224 + sectionLabel: {
225 + fontSize: 11,
226 + fontWeight: "600",
227 + letterSpacing: 0.8,
228 + color: Colors.light.textSecondary,
229 + textTransform: "uppercase",
230 + },
231 +
232 + // Annotation
233 + annotation: {
234 + fontSize: 14,
235 + lineHeight: 22,
236 + color: Colors.light.textSecondary,
237 + fontStyle: "italic",
238 + padding: 4,
239 + },
240 + });
frontend/app/triggers/new.tsx
1 1
import {
2 2
View,
3 3
Text,
4 4
StyleSheet,
5 5
KeyboardAvoidingView,
▸ 9 unchanged lines
15 15
import { Spacing, Typography, Colors, BorderRadius } from "@/constants/theme";
16 16
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
17 17
import { useCreateTrigger, useMoodEntries } from "@/hooks";
18 18
import { CategoryChips, CategoryOption } from "@/components/ui/CategoryChips";
19 19
import { TriggerCategory } from "@/constants/triggers";
20 +
import { TRIGGER_CATEGORY_DEFINITIONS } from "@/constants/trigger-categories";
20 21
import { TimePicker } from "@/components/ui/TimePicker";
21 22
import { getMood } from "@/constants/moods";
22 23
import { LinkRow } from "@/components/ui/LinkRow";
23 24
24 -
const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] = [
25 -
{
26 -
label: "Trabalho",
27 -
value: "WORK",
28 -
icon: <MaterialIcons size={16} color="#60A5FA" name="work" />,
29 -
},
30 -
{
31 -
label: "Social",
32 -
value: "SOCIAL",
33 -
icon: <MaterialIcons size={16} color="#AF52DE" name="diversity-1" />,
34 -
},
35 -
{
36 -
label: "Saúde",
37 -
value: "HEALTH",
38 -
icon: <MaterialIcons size={16} color="#34C759" name="healing" />,
39 -
},
40 -
{
41 -
label: "Família",
42 -
value: "FAMILY",
43 -
icon: <MaterialIcons size={16} color="#FF9500" name="family-restroom" />,
44 -
},
45 -
{
46 -
label: "Outros",
47 -
value: "OTHER",
48 -
icon: <Ionicons size={16} color="#8E8E93" name="ellipsis-horizontal" />,
49 -
},
50 -
];
25 +
const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] =
26 +
TRIGGER_CATEGORY_DEFINITIONS.map((d) => ({
27 +
label: d.label,
28 +
value: d.id,
29 +
icon: <MaterialIcons size={16} color={d.color} name={d.icon} />,
30 +
}));
51 31
52 32
export default function NewTrigger() {
53 33
const [moment, setMoment] = useState(new Date());
54 34
const [comment, setComment] = useState("");
55 35
const [category, setCategory] = useState("WORK");
▸ 172 unchanged lines
228 208
alignItems: "center",
229 209
justifyContent: "center",
230 210
marginTop: 1,
231 211
},
232 212
});
frontend/components/history/TriggerCard.tsx
1 1
import { Text, View, 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 { HistoryCard } from "@/lib/history/types";
6 6
import { Trigger } from "@/lib/api";
7 7
import { Spacing } from "@/constants/theme";
8 -
9 -
// TODO: Move these to a constant as well
10 -
const TRIGGER_META: Record<
11 -
string,
12 -
{ label: string; variant: string; icon: keyof typeof MaterialIcons.glyphMap }
13 -
> = {
14 -
SOCIAL: { label: "Social", variant: "blue", icon: "diversity-1" },
15 -
FAMILY: { label: "Familiar", variant: "green", icon: "family-restroom" },
16 -
HEALTH: { label: "Emocional", variant: "purple", icon: "healing" },
17 -
PHYSICAL: { label: "Físico", variant: "orange", icon: "fitness-center" },
18 -
WORK: { label: "Trabalho", variant: "blue", icon: "work" },
19 -
OTHER: { label: "Outro", variant: "gray", icon: "help-outline" },
20 -
};
8 +
import { router } from 'expo-router';
9 +
import { getTriggerCategory } from "@/constants/trigger-categories";
21 10
22 11
type Props = { card: HistoryCard & { raw: Trigger } };
23 12
24 13
export function TriggerHistoryCard({ card }: Props) {
25 -
const meta = TRIGGER_META[card.raw.category] ?? TRIGGER_META.OTHER;
14 +
const meta = getTriggerCategory(card.raw.category);
26 15
const time = new Date(card.timestamp).toLocaleTimeString("pt-BR", {
27 16
hour: "2-digit",
28 17
minute: "2-digit",
29 18
});
30 19
31 20
return (
32 -
<Card style={styles.card}>
21 +
<Card style={styles.card} onPress={() => router.push(`/triggers/${card.raw.id}`) }>
33 22
<View style={styles.row}>
34 -
<View style={[styles.iconWrap, { backgroundColor: "#fff7ed" }]}>
35 -
<MaterialIcons name={meta.icon} size={22} color="#f97316" />
23 +
<View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
24 +
<MaterialIcons name={meta.icon} size={22} color={meta.color} />
36 25
</View>
37 26
<View style={styles.body}>
38 27
<View style={styles.titleRow}>
39 28
<Text style={styles.title}>Gatilho Identificado</Text>
40 29
<Text style={styles.time}>{time}</Text>
▸ 37 unchanged lines
78 67
title: { fontSize: 15, fontWeight: "600" },
79 68
time: { fontSize: 13, opacity: 0.45 },
80 69
summary: { fontSize: 13, opacity: 0.6, lineHeight: 18, marginBottom: 6 },
81 70
badge: { alignSelf: "flex-start", marginTop: 4 },
82 71
});
frontend/constants/trigger-categories.ts
@@ -0,0 +1,24 @@
1 + import { MaterialIcons } from "@expo/vector-icons";
2 + import { TriggerCategory } from "@/constants/triggers";
3 +
4 + export interface TriggerCategoryDefinition {
5 + id: TriggerCategory;
6 + label: string;
7 + icon: keyof typeof MaterialIcons.glyphMap;
8 + color: string;
9 + iconBackground: string;
10 + variant: string;
11 + }
12 +
13 + export const TRIGGER_CATEGORY_DEFINITIONS: TriggerCategoryDefinition[] = [
14 + { id: "WORK", label: "Trabalho", icon: "work", color: "#3b82f6", iconBackground: "#dbeafe", variant: "blue" },
15 + { id: "SOCIAL", label: "Social", icon: "diversity-1", color: "#8b5cf6", iconBackground: "#ede9fe", variant: "purple" },
16 + { id: "HEALTH", label: "Saúde", icon: "healing", color: "#34c272", iconBackground: "#d4f5e2", variant: "green" },
17 + { id: "FAMILY", label: "Família", icon: "family-restroom", color: "#f97316", iconBackground: "#ffedd5", variant: "orange" },
18 + { id: "PHYSICAL", label: "Físico", icon: "fitness-center", color: "#f97316", iconBackground: "#ffedd5", variant: "orange" },
19 + { id: "OTHER", label: "Outro", icon: "help-outline", color: "#94a3b8", iconBackground: "#f1f5f9", variant: "gray" },
20 + ];
21 +
22 + export const getTriggerCategory = (id: string): TriggerCategoryDefinition =>
23 + TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === id) ??
24 + TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === "OTHER")!;