frontend: reorganizing triggers' metadata
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
fd21c7f08eddd7a1198b4c46080186820aa30a6c Browse filesParents
e2b786dChanged files
4
Additions
+277
Deletions
-44
/dev/null → frontend/app/triggers/[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, 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,
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");
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>
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
});
/dev/null → frontend/constants/trigger-categories.ts
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")!;