frontend: display post-mood add page and link trigger
Parents:
5f371a66 file(s) changed
- frontend/app/(tabs)/new/_layout.tsx +1 -0
- frontend/app/(tabs)/new/entry.tsx +4 -2
- frontend/app/(tabs)/new/post-mood.tsx +364 -0
- frontend/app/entry/post-mood-links.tsx +198 -0
- frontend/app/triggers/new.tsx +15 -4
- frontend/hooks/useTrigger.queries.ts +17 -0
frontend/app/(tabs)/new/_layout.tsx
1 1
import { Stack } from 'expo-router';
2 2
3 3
export const unstable_settings = {
4 4
initialRouteName: 'index'
5 5
};
▸ 7 unchanged lines
13 13
<Stack.Screen name="index" />
14 14
<Stack.Screen name="entry" options={{
15 15
headerTitle: "Registrar humor",
16 16
headerShown: true
17 17
}} />
18 +
<Stack.Screen name="post-mood" options={{ headerShown: false }} />
18 19
</Stack>
19 20
);
20 21
}
frontend/app/(tabs)/new/entry.tsx
1 1
import { useState, useEffect } from "react";
2 2
import { Link, router, useLocalSearchParams } from "expo-router";
3 3
4 4
import { View, Text, StyleSheet, ScrollView } from "react-native";
5 5
▸ 55 unchanged lines
61 61
component: c.id.toUpperCase(),
62 62
intensity: intensityToValue(c.intensity),
63 63
})),
64 64
};
65 65
66 -
await createMoodEntry.mutateAsync(data);
66 +
const result = await createMoodEntry.mutateAsync(data);
67 +
const moodId = result.mood.id;
67 68
68 69
reset();
69 -
router.replace("/");
70 +
71 +
router.replace(`/new/post-mood?moodId=${moodId}`);
70 72
};
71 73
72 74
const editComponents = () => {
73 75
router.push("/entry/mood-components");
74 76
};
▸ 123 unchanged lines
198 200
},
199 201
resumeCard: {
200 202
padding: Spacing.cardGap,
201 203
},
202 204
});
frontend/app/(tabs)/new/post-mood.tsx
@@ -0,0 +1,364 @@
1 + import { useState } from "react";
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + TouchableOpacity,
7 + Pressable,
8 + ActivityIndicator,
9 + } from "react-native";
10 + import { router, useLocalSearchParams } from "expo-router";
11 + import { Ionicons, MaterialIcons } from "@expo/vector-icons";
12 +
13 + import { ScreenLayout } from "@/components/ui/ScreenLayout";
14 + import { Card } from "@/components/ui/Cards";
15 + import { Button } from "@/components/ui/Button";
16 + import { useAuth } from "@/context/AuthContext";
17 + import { Colors, Spacing, Typography, BorderRadius, Shadows } from "@/constants/theme";
18 + import { getTriggerCategory } from "@/constants/trigger-categories";
19 + import { useTriggers, useLinkMoodToTrigger } from "@/hooks";
20 + import { Trigger } from "@/lib/api";
21 + import { formatMoment } from '@/lib/utils/time';
22 +
23 + export default function PostMood() {
24 + const { moodId } = useLocalSearchParams<{ moodId: string }>();
25 + const { user } = useAuth();
26 +
27 + const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
28 + const [impact, setImpact] = useState(3);
29 + const [linked, setLinked] = useState(false);
30 +
31 + const { data: triggersData, isLoading: isLoadingTriggers } = useTriggers({ limit: 3 });
32 + const recentTriggers = triggersData?.entries ?? [];
33 +
34 + const linkMoodToTrigger = useLinkMoodToTrigger();
35 +
36 + const handleLink = async () => {
37 + if (!selectedTrigger || !moodId) return;
38 + await linkMoodToTrigger.mutateAsync({
39 + triggerId: String(selectedTrigger.id),
40 + moodId,
41 + perceivedImpact: impact,
42 + });
43 + setLinked(true);
44 + setSelectedTrigger(null);
45 + };
46 +
47 + const onNotificationPress = () => {
48 + router.push("/notifications");
49 + };
50 +
51 + return (
52 + <ScreenLayout
53 + userName={user?.firstName}
54 + userAvatar={user?.avatarURL}
55 + onNotificationPress={onNotificationPress}
56 + >
57 + {/* Confirmation block */}
58 + <View style={styles.confirmationBlock}>
59 + <View style={styles.outerRing}>
60 + <View style={styles.innerCircle}>
61 + <Ionicons name="checkmark" size={32} color="#fff" />
62 + </View>
63 + </View>
64 + <Text style={styles.confirmTitle}>Humor Salvo!</Text>
65 + <Text style={styles.confirmSubtitle}>
66 + Seu registro foi armazenado com sucesso.
67 + </Text>
68 + </View>
69 +
70 + {/* Link prompt card */}
71 + {linked ? (
72 + <View style={styles.linkedRow}>
73 + <Ionicons name="checkmark-circle" size={20} color={Colors.light.tint} />
74 + <Text style={styles.linkedText}>Gatilho vinculado</Text>
75 + </View>
76 + ) : (
77 + <Card style={styles.linkCard}>
78 + {selectedTrigger ? (
79 + /* Impact picker state */
80 + <View style={styles.impactPicker}>
81 + {/* Selected trigger header */}
82 + <View style={styles.selectedTriggerRow}>
83 + {(() => {
84 + const cat = getTriggerCategory(selectedTrigger.category);
85 + return (
86 + <>
87 + <View style={[styles.triggerIconBubble, { backgroundColor: cat.iconBackground }]}>
88 + <MaterialIcons name={cat.icon} size={18} color={cat.color} />
89 + </View>
90 + <Text style={styles.selectedTriggerLabel} numberOfLines={1}>
91 + {cat.label}
92 + </Text>
93 + </>
94 + );
95 + })()}
96 + <TouchableOpacity
97 + onPress={() => setSelectedTrigger(null)}
98 + hitSlop={8}
99 + style={styles.deselectButton}
100 + >
101 + <Ionicons name="close" size={18} color={Colors.light.textSecondary} />
102 + </TouchableOpacity>
103 + </View>
104 +
105 + <Text style={styles.impactLabel}>Qual foi o impacto?</Text>
106 +
107 + <View style={styles.impactButtons}>
108 + {[1, 2, 3, 4, 5].map((n) => {
109 + const isSelected = impact === n;
110 + return (
111 + <TouchableOpacity
112 + key={n}
113 + style={[
114 + styles.impactCircle,
115 + isSelected && styles.impactCircleSelected,
116 + ]}
117 + onPress={() => setImpact(n)}
118 + activeOpacity={0.7}
119 + >
120 + <Text
121 + style={[
122 + styles.impactCircleText,
123 + isSelected && styles.impactCircleTextSelected,
124 + ]}
125 + >
126 + {n}
127 + </Text>
128 + </TouchableOpacity>
129 + );
130 + })}
131 + </View>
132 +
133 + <Button
134 + title="Vincular"
135 + onPress={handleLink}
136 + loading={linkMoodToTrigger.isPending}
137 + style={{ marginTop: 4, ...Shadows.lg }}
138 + />
139 + </View>
140 + ) : (
141 + /* Trigger selection state */
142 + <>
143 + <Text style={styles.linkCardTitle}>
144 + Algo aconteceu que pode ter influenciado esse humor?
145 + </Text>
146 + <Text style={styles.linkCardSubtitle}>
147 + Selecione um gatilho recente ou adicione um novo.
148 + </Text>
149 +
150 + {isLoadingTriggers ? (
151 + <ActivityIndicator style={{ marginTop: 12 }} />
152 + ) : (
153 + <View style={styles.triggerGrid}>
154 + {recentTriggers.map((trigger) => {
155 + const cat = getTriggerCategory(trigger.category);
156 + return (
157 + <Pressable
158 + key={trigger.id}
159 + style={({ pressed }) => [
160 + styles.triggerGridItem,
161 + pressed && { opacity: 0.7 },
162 + ]}
163 + onPress={() => setSelectedTrigger(trigger)}
164 + >
165 + <View style={[styles.triggerIconBubble, { backgroundColor: cat.iconBackground }]}>
166 + <MaterialIcons name={cat.icon} size={20} color={cat.color} />
167 + </View>
168 + <Text style={styles.triggerItemLabel} numberOfLines={2}>
169 + {cat.label}
170 + </Text>
171 + <Text style={styles.triggerItemLabel} numberOfLines={2}>
172 + {formatMoment(trigger.moment)}
173 + </Text>
174 + </Pressable>
175 + );
176 + })}
177 +
178 + {/* New trigger item */}
179 + <Pressable
180 + style={({ pressed }) => [
181 + styles.triggerGridItem,
182 + styles.newTriggerItem,
183 + pressed && { opacity: 0.7 },
184 + ]}
185 + onPress={() => router.push(`/triggers/new?moodId=${moodId}`)}
186 + >
187 + <Ionicons
188 + name="add-circle-outline"
189 + size={24}
190 + color={Colors.light.textSecondary}
191 + />
192 + <Text style={styles.newTriggerLabel}>Novo gatilho</Text>
193 + </Pressable>
194 + </View>
195 + )}
196 + </>
197 + )}
198 + </Card>
199 + )}
200 + </ScreenLayout>
201 + );
202 + }
203 +
204 + const styles = StyleSheet.create({
205 + confirmationBlock: {
206 + alignItems: "center",
207 + marginBottom: Spacing.sectionGap,
208 + gap: 12,
209 + },
210 + outerRing: {
211 + width: 88,
212 + height: 88,
213 + borderRadius: 44,
214 + backgroundColor: "#E7FDEF",
215 + alignItems: "center",
216 + justifyContent: "center",
217 + },
218 + innerCircle: {
219 + width: 64,
220 + height: 64,
221 + borderRadius: 32,
222 + backgroundColor: Colors.light.tint,
223 + alignItems: "center",
224 + justifyContent: "center",
225 + },
226 + confirmTitle: {
227 + ...Typography.headlineMd,
228 + fontWeight: "700",
229 + color: Colors.light.text,
230 + textAlign: "center",
231 + },
232 + confirmSubtitle: {
233 + ...Typography.bodyMd,
234 + color: Colors.light.textSecondary,
235 + textAlign: "center",
236 + },
237 + linkCard: {
238 + padding: Spacing.cardGap,
239 + marginBottom: Spacing.sectionGap,
240 + },
241 + linkCardTitle: {
242 + ...Typography.bodyMd,
243 + fontWeight: "700",
244 + color: Colors.light.text,
245 + marginBottom: 4,
246 + },
247 + linkCardSubtitle: {
248 + ...Typography.bodyMd,
249 + color: Colors.light.textSecondary,
250 + marginBottom: 16,
251 + },
252 + triggerGrid: {
253 + flexDirection: "row",
254 + flexWrap: "wrap",
255 + gap: 10,
256 + },
257 + triggerGridItem: {
258 + width: "48%",
259 + minHeight: 80,
260 + backgroundColor: "#fff",
261 + borderRadius: BorderRadius.md,
262 + borderWidth: 1,
263 + borderColor: Colors.light.divider,
264 + alignItems: "center",
265 + justifyContent: "center",
266 + padding: 10,
267 + gap: 6,
268 + ...Shadows.sm,
269 + },
270 + newTriggerItem: {
271 + borderStyle: "dashed",
272 + borderColor: Colors.light.borderDashed,
273 + backgroundColor: "transparent",
274 + shadowColor: "#000000",
275 + shadowOffset: { width: 0, height: 0 },
276 + shadowOpacity: 0,
277 + shadowRadius: 0,
278 + elevation: 0,
279 + },
280 + triggerIconBubble: {
281 + width: 36,
282 + height: 36,
283 + borderRadius: 18,
284 + alignItems: "center",
285 + justifyContent: "center",
286 + },
287 + triggerItemLabel: {
288 + ...Typography.labelSm,
289 + color: Colors.light.text,
290 + textAlign: "center",
291 + },
292 + newTriggerLabel: {
293 + ...Typography.labelSm,
294 + color: Colors.light.textSecondary,
295 + textAlign: "center",
296 + },
297 + // Impact picker
298 + impactPicker: {
299 + gap: 12,
300 + },
301 + selectedTriggerRow: {
302 + flexDirection: "row",
303 + alignItems: "center",
304 + gap: 10,
305 + marginBottom: 4,
306 + },
307 + selectedTriggerLabel: {
308 + ...Typography.bodyMd,
309 + fontWeight: "600",
310 + color: Colors.light.text,
311 + flex: 1,
312 + },
313 + deselectButton: {
314 + padding: 4,
315 + },
316 + impactLabel: {
317 + ...Typography.bodyMd,
318 + fontWeight: "600",
319 + color: Colors.light.text,
320 + },
321 + impactButtons: {
322 + flexDirection: "row",
323 + gap: 10,
324 + justifyContent: "center",
325 + },
326 + impactCircle: {
327 + width: 48,
328 + height: 48,
329 + borderRadius: 24,
330 + backgroundColor: "#fff",
331 + borderWidth: 1.5,
332 + borderColor: Colors.light.divider,
333 + alignItems: "center",
334 + justifyContent: "center",
335 + },
336 + impactCircleSelected: {
337 + backgroundColor: Colors.light.tint + "1A",
338 + borderColor: Colors.light.tint,
339 + },
340 + impactCircleText: {
341 + fontSize: 16,
342 + fontWeight: "600",
343 + color: Colors.light.text,
344 + },
345 + impactCircleTextSelected: {
346 + color: Colors.light.text,
347 + },
348 + // Linked success
349 + linkedRow: {
350 + flexDirection: "row",
351 + alignItems: "center",
352 + gap: 8,
353 + marginBottom: Spacing.sectionGap,
354 + paddingHorizontal: 4,
355 + },
356 + linkedText: {
357 + ...Typography.bodyMd,
358 + fontWeight: "600",
359 + color: Colors.light.tint,
360 + },
361 + moodList: {
362 + gap: 8,
363 + },
364 + });
frontend/app/entry/post-mood-links.tsx
@@ -0,0 +1,198 @@
1 + import { View, Text, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
2 + import { useState } from 'react';
3 + import { router, useLocalSearchParams } from 'expo-router';
4 + import { Ionicons, MaterialIcons } from '@expo/vector-icons';
5 + import { Button } from '@/components/ui/Button';
6 + import { Section, SectionHeader } from '@/components/ui/Sections';
7 + import { Spacing, Typography, Colors, BorderRadius } from '@/constants/theme';
8 + import { useTriggers, useLinkMoodToTrigger } from '@/hooks';
9 + import { getTriggerCategory } from '@/constants/trigger-categories';
10 + import { Trigger } from '@/lib/api';
11 +
12 + type FlowState = 'PICK_TRIGGER' | 'PICK_IMPACT';
13 +
14 + export default function PostMoodLinks() {
15 + const { moodId } = useLocalSearchParams<{ moodId: string }>();
16 + const [flowState, setFlowState] = useState<FlowState>('PICK_TRIGGER');
17 + const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
18 + const [impact, setImpact] = useState<number>(3);
19 +
20 + const { data, isLoading } = useTriggers({ limit: 5 });
21 + const linkMood = useLinkMoodToTrigger();
22 +
23 + const triggers = data?.entries ?? [];
24 +
25 + const handleSelectTrigger = (trigger: Trigger) => {
26 + setSelectedTrigger(trigger);
27 + setFlowState('PICK_IMPACT');
28 + };
29 +
30 + const handleSaveLink = async () => {
31 + if (!selectedTrigger || !moodId) return;
32 + await linkMood.mutateAsync({
33 + triggerId: String(selectedTrigger.id),
34 + moodId,
35 + perceivedImpact: impact,
36 + });
37 + router.back();
38 + };
39 +
40 + if (flowState === 'PICK_TRIGGER') {
41 + return (
42 + <View style={styles.container}>
43 + <View style={styles.header}>
44 + <Text style={styles.title}>Algo influenciou esse humor?</Text>
45 + <Text style={styles.subtitle}>
46 + Selecione um gatilho recente para vincular
47 + </Text>
48 + </View>
49 +
50 + <ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
51 + {isLoading && <ActivityIndicator style={{ marginTop: 20 }} />}
52 +
53 + {!isLoading && triggers.length === 0 && (
54 + <Text style={styles.emptyText}>Nenhum gatilho recente</Text>
55 + )}
56 +
57 + {triggers.map((trigger) => {
58 + const cat = getTriggerCategory(trigger.category);
59 + return (
60 + <TouchableOpacity
61 + key={trigger.id}
62 + style={styles.triggerCard}
63 + activeOpacity={0.7}
64 + onPress={() => handleSelectTrigger(trigger)}
65 + >
66 + <View style={[styles.triggerIcon, { backgroundColor: cat.iconBackground }]}>
67 + <MaterialIcons name={cat.icon} size={18} color={cat.color} />
68 + </View>
69 + <View style={styles.triggerText}>
70 + <Text style={styles.triggerCategory}>{cat.label}</Text>
71 + {trigger.comment ? (
72 + <Text style={styles.triggerComment} numberOfLines={1}>{trigger.comment}</Text>
73 + ) : null}
74 + </View>
75 + <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
76 + </TouchableOpacity>
77 + );
78 + })}
79 +
80 + <Button title="Pular" variant="ghost" onPress={() => router.back()} />
81 + </ScrollView>
82 + </View>
83 + );
84 + }
85 +
86 + // PICK_IMPACT
87 + const cat = selectedTrigger ? getTriggerCategory(selectedTrigger.category) : null;
88 + return (
89 + <View style={styles.container}>
90 + <View style={styles.header}>
91 + <Text style={styles.title}>Qual foi o impacto?</Text>
92 + <Text style={styles.subtitle}>De 1 (leve) a 5 (muito forte)</Text>
93 + </View>
94 +
95 + {selectedTrigger && cat && (
96 + <View style={styles.selectedTrigger}>
97 + <View style={[styles.triggerIcon, { backgroundColor: cat.iconBackground }]}>
98 + <MaterialIcons name={cat.icon} size={18} color={cat.color} />
99 + </View>
100 + <Text style={styles.selectedTriggerLabel}>{cat.label}</Text>
101 + </View>
102 + )}
103 +
104 + <View style={styles.impactRow}>
105 + {[1, 2, 3, 4, 5].map((v) => (
106 + <TouchableOpacity
107 + key={v}
108 + style={[styles.impactDot, impact === v && styles.impactDotActive]}
109 + activeOpacity={0.7}
110 + onPress={() => setImpact(v)}
111 + >
112 + <Text style={[styles.impactLabel, impact === v && styles.impactLabelActive]}>{v}</Text>
113 + </TouchableOpacity>
114 + ))}
115 + </View>
116 +
117 + <Button
118 + title="Vincular"
119 + onPress={handleSaveLink}
120 + loading={linkMood.isPending}
121 + style={{ marginTop: 8 }}
122 + />
123 + <Button
124 + title="Voltar"
125 + variant="ghost"
126 + onPress={() => setFlowState('PICK_TRIGGER')}
127 + />
128 + </View>
129 + );
130 + }
131 +
132 + const styles = StyleSheet.create({
133 + container: {
134 + flex: 1,
135 + paddingHorizontal: Spacing.containerPadding,
136 + paddingTop: 24,
137 + paddingBottom: 36,
138 + gap: 16,
139 + },
140 + header: { gap: 6 },
141 + title: { ...Typography.headlineMd },
142 + subtitle: { ...Typography.bodyMd, color: Colors.light.textSecondary },
143 + scrollContent: { gap: 10, paddingBottom: 20 },
144 + emptyText: { ...Typography.bodyMd, color: Colors.light.textSecondary, textAlign: 'center', marginTop: 20 },
145 + triggerCard: {
146 + flexDirection: 'row',
147 + alignItems: 'center',
148 + backgroundColor: '#fff',
149 + borderRadius: BorderRadius.lg,
150 + padding: Spacing.cardGap,
151 + borderWidth: 1,
152 + borderColor: Colors.light.divider,
153 + gap: 12,
154 + },
155 + triggerIcon: {
156 + width: 36,
157 + height: 36,
158 + borderRadius: BorderRadius.md,
159 + alignItems: 'center',
160 + justifyContent: 'center',
161 + },
162 + triggerText: { flex: 1, gap: 2 },
163 + triggerCategory: { ...Typography.bodyMd, fontWeight: '600' },
164 + triggerComment: { ...Typography.labelSm, color: Colors.light.textSecondary },
165 + selectedTrigger: {
166 + flexDirection: 'row',
167 + alignItems: 'center',
168 + gap: 12,
169 + padding: Spacing.cardGap,
170 + backgroundColor: Colors.light.surface,
171 + borderRadius: BorderRadius.lg,
172 + borderWidth: 1,
173 + borderColor: Colors.light.divider,
174 + },
175 + selectedTriggerLabel: { ...Typography.bodyMd, fontWeight: '600' },
176 + impactRow: {
177 + flexDirection: 'row',
178 + justifyContent: 'center',
179 + gap: 12,
180 + marginVertical: 8,
181 + },
182 + impactDot: {
183 + width: 48,
184 + height: 48,
185 + borderRadius: 24,
186 + borderWidth: 2,
187 + borderColor: Colors.light.divider,
188 + alignItems: 'center',
189 + justifyContent: 'center',
190 + backgroundColor: '#fff',
191 + },
192 + impactDotActive: {
193 + borderColor: Colors.light.tint,
194 + backgroundColor: Colors.light.tint + '1A',
195 + },
196 + impactLabel: { fontSize: 18, fontWeight: '600', color: Colors.light.textSecondary },
197 + impactLabelActive: { color: Colors.light.text },
198 + });
frontend/app/triggers/new.tsx
1 1
import {
2 2
View,
3 3
Text,
4 4
StyleSheet,
5 5
KeyboardAvoidingView,
▸ 6 unchanged lines
12 12
import { TextArea } from "@/components/ui/Input";
13 13
import { Button } from "@/components/ui/Button";
14 14
import { Section, SectionHeader } from "@/components/ui/Sections";
15 15
import { Spacing, Typography, Colors, BorderRadius } from "@/constants/theme";
16 16
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
17 -
import { useCreateTrigger, useMoodEntries } from "@/hooks";
17 +
import { useCreateTrigger, useMoodEntries, useLinkMoodToTrigger } from "@/hooks";
18 18
import { CategoryChips, CategoryOption } from "@/components/ui/CategoryChips";
19 19
import { TriggerCategory } from "@/constants/triggers";
20 20
import { TRIGGER_CATEGORY_DEFINITIONS } from "@/constants/trigger-categories";
21 21
import { TimePicker } from "@/components/ui/TimePicker";
22 22
import { getMood } from "@/constants/moods";
▸ 7 unchanged lines
30 30
}));
31 31
32 32
export default function NewTrigger() {
33 33
const [moment, setMoment] = useState(new Date());
34 34
const [comment, setComment] = useState("");
35 -
const { category: initialCategory } = useLocalSearchParams<{ category?: string }>();
35 +
const { category: initialCategory, moodId: linkedMoodId } = useLocalSearchParams<{ category?: string; moodId?: string }>();
36 36
const [category, setCategory] = useState(initialCategory ?? "WORK");
37 37
const createTrigger = useCreateTrigger();
38 +
const linkMoodToTrigger = useLinkMoodToTrigger();
38 39
const [linkMood, setLinkMood] = useState(false);
39 40
const { data: moodEntries, isLoading: isLoadingMood } = useMoodEntries({
40 41
limit: 1,
41 42
});
42 43
▸ 8 unchanged lines
51 52
comment,
52 53
category,
53 54
...(linkMood && latestMood ? { moodId: latestMood.id } : null),
54 55
};
55 56
56 -
await createTrigger.mutateAsync(data);
57 -
router.replace("/(tabs)/actions");
57 +
const result = await createTrigger.mutateAsync(data);
58 +
59 +
if (linkedMoodId) {
60 +
await linkMoodToTrigger.mutateAsync({
61 +
triggerId: String(result.trigger.id),
62 +
moodId: linkedMoodId,
63 +
perceivedImpact: 3,
64 +
});
65 +
router.back();
66 +
} else {
67 +
router.replace("/(tabs)/actions");
68 +
}
58 69
};
59 70
60 71
return (
61 72
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
62 73
<ScrollView scrollEventThrottle={16}>
▸ 146 unchanged lines
209 220
alignItems: "center",
210 221
justifyContent: "center",
211 222
marginTop: 1,
212 223
},
213 224
});
frontend/hooks/useTrigger.queries.ts
1 1
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2 2
3 3
import { apiClient, Trigger, CreateTriggerPayload, UpdateTriggerPayload } from "@/lib/api";
4 4
import { insightKeys } from "@/hooks/useInsights.queries";
5 5
▸ 125 unchanged lines
131 131
}, 2000);
132 132
},
133 133
});
134 134
};
135 135
136 +
export const useLinkMoodToTrigger = () => {
137 +
const queryClient = useQueryClient();
138 +
139 +
return useMutation({
140 +
mutationFn: ({ triggerId, moodId, perceivedImpact }: {
141 +
triggerId: string;
142 +
moodId: string;
143 +
perceivedImpact: number
144 +
}) =>
145 +
apiClient.linkMoodToTrigger(triggerId, { moodId: Number(moodId), perceivedImpact }),
146 +
147 +
onSuccess: (_data, { triggerId }) => {
148 +
queryClient.invalidateQueries({ queryKey: triggerKeys.detail(triggerId) });
149 +
},
150 +
});
151 +
};
152 +
136 153
// Delete a mood entry with optimistic removal
137 154
export const useDeleteTrigger = () => {
138 155
const queryClient = useQueryClient();
139 156
140 157
return useMutation({
▸ 22 unchanged lines
163 180
queryClient.invalidateQueries({ queryKey: insightKeys.byType("TRIGGER_PATTERN") });
164 181
}, 2000);
165 182
},
166 183
});
167 184
};