frontend: introduce triggers hooks + dispatch save to the API
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
4ef759a7c84b0f1067b873dcf561cc752569a817 Browse filesParents
cd79c89Changed files
6
Additions
+314
Deletions
-132
frontend/app/triggers/new.tsx
1 1
import {
2 2
View,
3 3
Text,
4 4
StyleSheet,
5 5
KeyboardAvoidingView,
6 -
ScrollView
7 -
} from 'react-native';
8 -
import { useEffect, useState } from 'react';
9 -
import { router } from 'expo-router';
6 +
ScrollView,
7 +
} from "react-native";
8 +
import { useEffect, useState } from "react";
9 +
import { router } from "expo-router";
10 10
11 -
import { Card } from '@/components/ui/Cards';
12 -
import { Input, TextArea } from '@/components/ui/Input';
13 -
import { Button } from '@/components/ui/Button';
14 -
import { Section, SectionHeader } from '@/components/ui/Sections';
15 -
import { Spacing, Typography, Colors, BorderRadius } from '@/constants/theme';
16 -
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
17 -
import { useCreateSleepRecord } from '@/hooks';
18 -
import { CategoryChips, CategoryOption } from '@/components/ui/CategoryChips';
19 -
import { TriggerCategory } from '@/constants/triggers';
11 +
import { Card } from "@/components/ui/Cards";
12 +
import { Input, TextArea } from "@/components/ui/Input";
13 +
import { Button } from "@/components/ui/Button";
14 +
import { Section, SectionHeader } from "@/components/ui/Sections";
15 +
import { Spacing, Typography, Colors, BorderRadius } from "@/constants/theme";
16 +
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
17 +
import { useCreateTrigger } from "@/hooks";
18 +
import { CategoryChips, CategoryOption } from "@/components/ui/CategoryChips";
19 +
import { TriggerCategory } from "@/constants/triggers";
20 20
21 21
const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] = [
22 22
{
23 -
label: 'Trabalho',
24 -
value: 'work',
25 -
icon: <Ionicons size={16} color="#60A5FA" name="briefcase" />
23 +
label: "Trabalho",
24 +
value: "WORK",
25 +
icon: <Ionicons size={16} color="#60A5FA" name="briefcase" />,
26 26
},
27 27
{
28 -
label: 'Social',
29 -
value: 'social',
30 -
icon: <MaterialIcons size={16} color="#AF52DE" name="diversity-1" />
28 +
label: "Social",
29 +
value: "SOCIAL",
30 +
icon: <MaterialIcons size={16} color="#AF52DE" name="diversity-1" />,
31 31
},
32 32
{
33 -
label:
34 -
'Saúde',
35 -
value: 'health',
36 -
icon: <MaterialIcons size={16} color="#34C759" name="healing" />
33 +
label: "Saúde",
34 +
value: "HEALTH",
35 +
icon: <MaterialIcons size={16} color="#34C759" name="healing" />,
37 36
},
38 37
{
39 -
label: 'Família',
40 -
value: 'family',
41 -
icon: <MaterialIcons size={16} color="#FF9500" name="family-restroom" />
38 +
label: "Família",
39 +
value: "FAMILY",
40 +
icon: <MaterialIcons size={16} color="#FF9500" name="family-restroom" />,
42 41
},
43 42
{
44 -
label: 'Outros',
45 -
value: 'other',
46 -
icon: <Ionicons size={16} color="#8E8E93" name="ellipsis-horizontal" />
43 +
label: "Outros",
44 +
value: "OTHER",
45 +
icon: <Ionicons size={16} color="#8E8E93" name="ellipsis-horizontal" />,
47 46
},
48 47
];
49 48
50 49
export default function NewTrigger() {
51 -
const [moment, setMoment] = useState(new Date())
52 -
const [comment, setComment] = useState();
53 -
const [category, setCategory] = useState('work');
50 +
const [moment, setMoment] = useState(new Date());
51 +
const [comment, setComment] = useState("");
52 +
const [category, setCategory] = useState("WORK");
53 +
const createTrigger = useCreateTrigger();
54 54
55 55
const save = async () => {
56 56
const data = {
57 57
moment,
58 58
comment,
59 +
category,
59 60
};
60 61
61 -
console.log("Trigger data: ", data)
62 -
//await createSleepRecord.mutateAsync(data)
63 -
router.replace("/")
62 +
await createTrigger.mutateAsync(data);
63 +
router.replace("/(tabs)/actions");
64 64
};
65 65
66 66
return (
67 -
<KeyboardAvoidingView
68 -
behavior='height'
69 -
style={styles.keyboardView}>
67 +
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
70 68
<ScrollView scrollEventThrottle={16}>
71 69
<View style={styles.container}>
72 70
<View style={styles.header}>
73 71
<View style={styles.innerHeaderWrapper}>
74 -
<View style={[styles.headerIcon, { backgroundColor: '#FFF7ED' }]}>
72 +
<View style={[styles.headerIcon, { backgroundColor: "#FFF7ED" }]}>
75 73
<Ionicons name="flash-outline" size={20} color="#EA580C" />
76 74
</View>
77 75
78 76
<View style={styles.headerTitleSubtitle}>
79 77
<Text style={styles.headerTitle}>Aconteceu algo?</Text>
82 80
</Text>
83 81
</View>
84 82
</View>
85 83
86 84
<Text style={styles.description}>
87 -
Identificar gatilhos ajuda a entender melhor seus padrões emocionais
85 +
Identificar gatilhos ajuda a entender melhor seus padrões
86 +
emocionais
88 87
</Text>
89 88
</View>
90 89
91 90
<Section>
92 91
<SectionHeader title="Selecionar uma categoria" />
93 92
<CategoryChips
94 93
options={TRIGGER_CATEGORIES}
95 94
active={category}
96 -
onChange={setCategory} />
95 +
onChange={setCategory}
96 +
/>
97 97
</Section>
98 98
99 99
<Section>
100 100
<SectionHeader title="Adicione detalhes" />
101 101
102 102
<Card style={styles.mainCard}>
103 103
<TextArea
104 104
label="Comentários sobre"
105 -
type="text"
106 105
variant="darkGhost"
107 106
onChangeText={(val) => setComment(val)}
108 107
value={comment}
109 108
minRows={5}
110 109
maxRows={10}
111 110
placeholder="Descreva o que gerou esse sentimento"
112 -
style={styles.notes}
113 111
/>
114 112
</Card>
115 113
</Section>
116 114
117 115
<Button title="Salvar Registro" onPress={save} />
124 122
const styles = StyleSheet.create({
125 123
container: {
126 124
gap: 24,
127 125
paddingHorizontal: Spacing.containerPadding,
128 126
paddingVertical: Spacing.sectionGap,
129 -
marginBottom: 50
127 +
marginBottom: 50,
130 128
},
131 129
mainCard: {
132 -
padding: Spacing.cardGap
130 +
padding: Spacing.cardGap,
133 131
},
134 132
// Header
135 -
header: {
136 -
},
133 +
header: {},
137 134
headerIcon: {
138 135
paddingVertical: 15,
139 136
paddingHorizontal: 14,
140 137
width: 48,
141 138
borderRadius: BorderRadius.md,
142 139
},
143 140
innerHeaderWrapper: {
144 141
flex: 1,
145 -
flexDirection: 'row',
146 -
alignItems: 'center'
142 +
flexDirection: "row",
143 +
alignItems: "center",
147 144
},
148 145
headerTitleSubtitle: {
149 -
marginLeft: 10
146 +
marginLeft: 10,
150 147
},
151 148
headerTitle: {
152 -
...Typography.headlineMd
149 +
...Typography.headlineMd,
153 150
},
154 151
headerSubtitle: {
155 152
...Typography.bodyMd,
156 -
color: Colors.light.textSecondary
153 +
color: Colors.light.textSecondary,
157 154
},
158 155
description: {
159 156
...Typography.bodyMd,
160 157
color: Colors.light.textSecondary,
161 -
marginTop: 20
158 +
marginTop: 20,
162 159
},
163 160
keyboardView: {
164 161
flex: 1,
165 162
},
166 -
})
163 +
});
frontend/constants/triggers.ts
1 -
export type TriggerCategory = 'work' | 'social' | 'health' | 'family' | 'other';
1 +
export type TriggerCategory =
2 +
| "WORK"
3 +
| "SOCIAL"
4 +
| "HEALTH"
5 +
| "PHYSICAL"
6 +
| "FAMILY"
7 +
| "OTHER";
frontend/hooks/index.ts
1 1
// Legacy shit from react native
2 -
export {
3 -
useThemeColor
4 -
} from '@/hooks/use-theme-color';
2 +
export { useThemeColor } from "@/hooks/use-theme-color";
5 3
6 -
export {
7 -
useColorScheme
8 -
} from '@/hooks/use-color-scheme';
4 +
export { useColorScheme } from "@/hooks/use-color-scheme";
9 5
10 6
// Custom stuff
11 7
export {
12 8
moodKeys,
13 9
useMoodEntries,
14 10
useMoodEntriesInfinite,
15 11
useMoodEntry,
16 12
useCreateMoodEntry,
17 -
useDeleteMoodEntry
18 -
} from '@/hooks/useMoodEntries.queries';
13 +
useDeleteMoodEntry,
14 +
} from "@/hooks/useMoodEntries.queries";
19 15
20 16
export {
21 17
useSleepRecords,
22 -
useCreateSleepRecord
23 -
} from '@/hooks/useSleepRecord.queries';
18 +
useCreateSleepRecord,
19 +
} from "@/hooks/useSleepRecord.queries";
20 +
21 +
export { useTriggers, useCreateTrigger } from "@/hooks/useTrigger.queries";
/dev/null → frontend/hooks/useTrigger.queries.ts
1 +
import {
2 +
useQuery,
3 +
useMutation,
4 +
useQueryClient,
5 +
useInfiniteQuery,
6 +
} from "@tanstack/react-query";
7 +
8 +
import { apiClient, Trigger, CreateTriggerPayload } from "@/lib/api";
9 +
10 +
export const triggerKeys = {
11 +
all: () => ["triggers"] as const,
12 +
lists: () => [...triggerKeys.all(), "list"] as const,
13 +
list: (filters?: TriggerFilters) =>
14 +
[...triggerKeys.lists(), filters] as const,
15 +
detail: (id: string) => [...triggerKeys.all(), "detail", id] as const,
16 +
};
17 +
18 +
interface TriggerFilters {
19 +
from?: string; // ISO date
20 +
to?: string; // ISO date
21 +
limit?: number;
22 +
}
23 +
24 +
// Queries
25 +
26 +
// Fetch a paginated list of mood entries.
27 +
// On cold start, returns cached data instantly then revalidates.
28 +
export const useTriggers = (filters?: TriggerFilters) => {
29 +
return useQuery({
30 +
queryKey: triggerKeys.list(filters),
31 +
queryFn: () => apiClient.getTriggers(filters),
32 +
});
33 +
};
34 +
35 +
// Single entry detail
36 +
export const useTrigger = (id: string) => {
37 +
return useQuery({
38 +
queryKey: triggerKeys.detail(id),
39 +
queryFn: () => apiClient.getTrigger(id),
40 +
enabled: !!id,
41 +
});
42 +
};
43 +
44 +
// MUTATIONS
45 +
46 +
// Create a new mood entry
47 +
// Optimistically adds to cache, rolls back on failure
48 +
export const useCreateTrigger = () => {
49 +
const queryClient = useQueryClient();
50 +
51 +
return useMutation({
52 +
mutationFn: (payload: CreateTriggerPayload) =>
53 +
apiClient.createTrigger(payload),
54 +
55 +
// Optimistic update so that the UI reflects the new entry immediately
56 +
onMutate: async (payload) => {
57 +
// Cancel any in-flight refetches so they don't overwrite our optimistic update
58 +
await queryClient.cancelQueries({ queryKey: triggerKeys.lists() });
59 +
60 +
// Snapshot current cache so we can roll back
61 +
const previous = queryClient.getQueryData(triggerKeys.list());
62 +
63 +
// Inject a temporary optimistic record
64 +
// TODO: might as well create a shared attrs that we're keeping it
65 +
queryClient.setQueryData(triggerKeys.list(), (old: Trigger[] = []) => [
66 +
{
67 +
...payload,
68 +
id: `temp-${Date.now()}`,
69 +
createdAt: new Date().toISOString(),
70 +
_optimistic: true,
71 +
},
72 +
...old,
73 +
]);
74 +
75 +
return { previous };
76 +
},
77 +
78 +
// On error, roll back to the snapshot
79 +
onError: (_err, _payload, context) => {
80 +
if (context?.previous) {
81 +
queryClient.setQueryData(triggerKeys.list(), context.previous);
82 +
}
83 +
},
84 +
85 +
// On success, replace optimistic record with real one from server
86 +
onSuccess: () => {
87 +
queryClient.invalidateQueries({ queryKey: triggerKeys.lists() });
88 +
},
89 +
});
90 +
};
91 +
92 +
// Delete a mood entry with optimistic removal
93 +
export const useDeleteTrigger = () => {
94 +
const queryClient = useQueryClient();
95 +
96 +
return useMutation({
97 +
mutationFn: (id: string) => apiClient.deleteTrigger(id),
98 +
99 +
onMutate: async (id) => {
100 +
await queryClient.cancelQueries({ queryKey: triggerKeys.lists() });
101 +
const previous = queryClient.getQueryData(triggerKeys.list());
102 +
103 +
queryClient.setQueryData(triggerKeys.list(), (old: Trigger[] = []) =>
104 +
old.filter((e) => e.id !== id),
105 +
);
106 +
107 +
return { previous };
108 +
},
109 +
110 +
onError: (_err, _id, context) => {
111 +
if (context?.previous) {
112 +
queryClient.setQueryData(triggerKeys.list(), context.previous);
113 +
}
114 +
},
115 +
116 +
onSuccess: () => {
117 +
queryClient.invalidateQueries({ queryKey: triggerKeys.lists() });
118 +
},
119 +
});
120 +
};
frontend/lib/api/client.ts
1 -
import * as SecureStore from 'expo-secure-store';
1 +
import * as SecureStore from "expo-secure-store";
2 2
import {
3 3
User,
4 4
AuthResponse,
5 5
VerifyTokenResponse,
6 6
SignUpPayload,
9 9
UserUpdatePayload,
10 10
UserUpdateResponse,
11 11
MoodComponentPayload,
12 12
MoodEntryPayload,
13 13
SleepRecordPayload,
14 -
PaginatedResponse
15 -
} from '@/lib/api/types';
14 +
CreateTriggerPayload,
15 +
PaginatedResponse,
16 +
} from "@/lib/api/types";
16 17
17 18
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
18 19
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
19 20
20 21
class ApiClient {
21 22
private async getStoredToken(): Promise<string | null> {
22 23
try {
23 24
return await SecureStore.getItemAsync(TOKEN_KEY);
24 25
} catch (error) {
25 -
console.error('Failed to get stored token:', error);
26 +
console.error("Failed to get stored token:", error);
26 27
return null;
27 28
}
28 29
}
29 30
30 31
private async storeToken(token: string): Promise<void> {
31 32
try {
32 33
await SecureStore.setItemAsync(TOKEN_KEY, token);
33 34
} catch (error) {
34 -
console.error('Failed to store token:', error);
35 +
console.error("Failed to store token:", error);
35 36
throw error;
36 37
}
37 38
}
38 39
39 40
private async removeToken(): Promise<void> {
40 41
try {
41 42
await SecureStore.deleteItemAsync(TOKEN_KEY);
42 43
} catch (error) {
43 -
console.error('Failed to remove token:', error);
44 +
console.error("Failed to remove token:", error);
44 45
}
45 46
}
46 47
47 48
async getStoredAuthData(): Promise<{ token: string | null }> {
48 49
const token = await this.getStoredToken();
49 50
return { token };
50 51
}
51 52
52 53
private async request<T>(
53 54
endpoint: string,
54 -
options: RequestInit = {}
55 +
options: RequestInit = {},
55 56
): Promise<T> {
56 57
const token = await this.getStoredToken();
57 58
const isFormData = options.body instanceof FormData;
58 59
59 60
const config: RequestInit = {
60 61
...options,
61 62
headers: {
62 63
...(token && { Authorization: `Bearer ${token}` }),
63 -
...(!isFormData && { 'Content-Type': 'application/json' }),
64 +
...(!isFormData && { "Content-Type": "application/json" }),
64 65
...options.headers,
65 66
},
66 67
};
67 68
68 69
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
69 70
70 71
if (!response.ok) {
71 -
const error = await response.json()
72 -
.catch(() => ({ error: 'Network error' }));
72 +
const error = await response
73 +
.json()
74 +
.catch(() => ({ error: "Network error" }));
73 75
74 -
console.error("[API]: ", error, response.status)
76 +
console.error("[API]: ", error, response.status);
75 77
76 78
throw new Error(error.error || `HTTP ${response.status}`);
77 79
}
78 80
79 81
return response.json();
80 82
}
81 83
82 84
// Auth
83 -
async login(email: string, password: string): Promise<AuthResponse | { isActive: boolean }> {
84 -
const response = await this.request<AuthResponse>('/auth/login', {
85 -
method: 'POST',
85 +
async login(
86 +
email: string,
87 +
password: string,
88 +
): Promise<AuthResponse | { isActive: boolean }> {
89 +
const response = await this.request<AuthResponse>("/auth/login", {
90 +
method: "POST",
86 91
body: JSON.stringify({ email, password }),
87 92
});
88 93
89 94
if (response.token) {
90 95
await this.storeToken(response.token);
92 97
93 98
return response;
94 99
}
95 100
96 101
async activate(code: number): Promise<ActivateResponse> {
97 -
return this.request('/auth/activate', {
98 -
method: 'POST',
102 +
return this.request("/auth/activate", {
103 +
method: "POST",
99 104
body: JSON.stringify({ code }),
100 105
});
101 106
}
102 107
103 108
async requestActivateCode(): Promise<void> {
104 -
return this.request('/auth/resend_code', {
105 -
method: 'POST'
106 -
})
109 +
return this.request("/auth/resend_code", {
110 +
method: "POST",
111 +
});
107 112
}
108 113
109 114
async signup(user: SignUpPayload): Promise<AuthResponse> {
110 -
const response = await this.request<AuthResponse>('/users', {
111 -
method: 'POST',
115 +
const response = await this.request<AuthResponse>("/users", {
116 +
method: "POST",
112 117
body: JSON.stringify({ user }),
113 118
});
114 119
115 120
await this.storeToken(response.token);
116 121
return response;
117 122
}
118 123
119 124
async forgotPassword(email: string): Promise<ResetPasswordRequestResponse> {
120 -
return this.request('/auth/forgot-password', {
121 -
method: 'POST',
125 +
return this.request("/auth/forgot-password", {
126 +
method: "POST",
122 127
body: JSON.stringify({ email }),
123 128
});
124 129
}
125 130
126 -
async resetPassword(token: string, password: string): Promise<ResetPasswordResponse> {
127 -
return this.request('/auth/reset-password', {
128 -
method: 'POST',
131 +
async resetPassword(
132 +
token: string,
133 +
password: string,
134 +
): Promise<ResetPasswordResponse> {
135 +
return this.request("/auth/reset-password", {
136 +
method: "POST",
129 137
body: JSON.stringify({ token, newPassword: password }),
130 138
});
131 139
}
132 140
133 141
async verifyToken(): Promise<VerifyTokenResponse> {
134 142
try {
135 -
return await this.request('/auth/verify');
143 +
return await this.request("/auth/verify");
136 144
} catch (error) {
137 145
// Token is invalid or expired
138 146
await this.removeToken();
139 147
throw error;
140 148
}
145 153
}
146 154
147 155
// User-related
148 156
async updateUser(user: UserUpdatePayload): Promise<User> {
149 157
return await this.request(`/users/${user.id}`, {
150 -
method: 'PUT',
151 -
body: JSON.stringify({ user })
152 -
})
158 +
method: "PUT",
159 +
body: JSON.stringify({ user }),
160 +
});
153 161
}
154 162
155 163
async avatar(formData: FormData): Promise<User> {
156 164
return await this.request(`/users/me/avatar`, {
157 -
method: 'POST',
165 +
method: "POST",
158 166
body: formData,
159 167
headers: {
160 -
'Content-Type': 'multipart/form-data',
168 +
"Content-Type": "multipart/form-data",
161 169
} as any,
162 -
})
170 +
});
163 171
}
164 172
165 173
async removeAvatar(): Promise<void> {
166 174
return await this.request(`/users/me/avatar`, {
167 -
method: 'DELETE'
175 +
method: "DELETE",
168 176
});
169 177
}
170 178
171 179
// Mood-related
172 -
async createMoodEntry(mood: CreateMoodEntryPayload): Promise<MoodEntryResponse> {
180 +
async createMoodEntry(
181 +
mood: CreateMoodEntryPayload,
182 +
): Promise<MoodEntryResponse> {
173 183
return await this.request(`/moods`, {
174 -
method: 'POST',
175 -
body: JSON.stringify({ mood })
176 -
})
184 +
method: "POST",
185 +
body: JSON.stringify({ mood }),
186 +
});
177 187
}
178 188
179 189
async getMoodEntries(params?: {
180 190
from?: string;
181 191
to?: string;
182 192
page?: number;
183 193
limit?: number;
184 194
}): Promise<PaginatedResponse<MoodEntry>> {
185 195
const query = new URLSearchParams();
186 -
if (params?.from) query.set('from', params.from);
187 -
if (params?.to) query.set('to', params.to);
188 -
if (params?.page) query.set('page', String(params.page));
189 -
if (params?.limit) query.set('limit', String(params.limit));
196 +
if (params?.from) query.set("from", params.from);
197 +
if (params?.to) query.set("to", params.to);
198 +
if (params?.page) query.set("page", String(params.page));
199 +
if (params?.limit) query.set("limit", String(params.limit));
190 200
191 201
const qs = query.toString();
192 -
return this.request(`/moods${qs ? `?${qs}` : ''}`);
202 +
return this.request(`/moods${qs ? `?${qs}` : ""}`);
193 203
}
194 204
195 205
async getMoodEntry(id: string): Promise<MoodEntry> {
196 206
return this.request(`/moods/${id}`);
197 207
}
198 208
199 209
async deleteMoodEntry(id: string): Promise<void> {
200 -
return this.request(`/moods/${id}`, { method: 'DELETE' });
210 +
return this.request(`/moods/${id}`, { method: "DELETE" });
201 211
}
202 212
203 213
// Sleep Record
204 -
async createSleepRecord(sleepRecord: CreateSleepRecordPayload): Promise<SleepRecordResponse> {
214 +
async createSleepRecord(
215 +
sleepRecord: CreateSleepRecordPayload,
216 +
): Promise<SleepRecordResponse> {
205 217
return await this.request(`/sleep_records`, {
206 -
method: 'POST',
207 -
body: JSON.stringify({ sleepRecord })
208 -
})
218 +
method: "POST",
219 +
body: JSON.stringify({ sleepRecord }),
220 +
});
209 221
}
210 222
211 223
async getSleepRecords(params?: {
212 224
from?: string;
213 225
to?: string;
214 226
page?: number;
215 227
limit?: number;
216 228
}): Promise<PaginatedResponse<SleepRecord>> {
217 229
const query = new URLSearchParams();
218 -
if (params?.from) query.set('from', params.from);
219 -
if (params?.to) query.set('to', params.to);
220 -
if (params?.page) query.set('page', String(params.page));
221 -
if (params?.limit) query.set('limit', String(params.limit));
230 +
if (params?.from) query.set("from", params.from);
231 +
if (params?.to) query.set("to", params.to);
232 +
if (params?.page) query.set("page", String(params.page));
233 +
if (params?.limit) query.set("limit", String(params.limit));
234 +
235 +
const qs = query.toString();
236 +
return this.request(`/sleep_records${qs ? `?${qs}` : ""}`);
237 +
}
238 +
239 +
// Trigger
240 +
async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
241 +
return await this.request(`/triggers`, {
242 +
method: "POST",
243 +
body: JSON.stringify({ trigger }),
244 +
});
245 +
}
246 +
247 +
async getTriggers(params?: {
248 +
from?: string;
249 +
to?: string;
250 +
page?: number;
251 +
limit?: number;
252 +
}): Promise<PaginatedResponse<Trigger>> {
253 +
const query = new URLSearchParams();
254 +
if (params?.from) query.set("from", params.from);
255 +
if (params?.to) query.set("to", params.to);
256 +
if (params?.page) query.set("page", String(params.page));
257 +
if (params?.limit) query.set("limit", String(params.limit));
222 258
223 259
const qs = query.toString();
224 -
return this.request(`/sleep_records${qs ? `?${qs}` : ''}`);
260 +
return this.request(`/triggers${qs ? `?${qs}` : ""}`);
261 +
}
262 +
263 +
async getTrigger(id: string): Promise<Trigger> {
264 +
return this.request(`/triggers/${id}`);
265 +
}
266 +
267 +
async deleteTrigger(id: string): Promise<void> {
268 +
return this.request(`/triggers/${id}`, { method: "DELETE" });
225 269
}
226 270
}
227 271
228 272
export const apiClient = new ApiClient();
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
6 6
updatedAt: Date;
7 7
avatarKey?: string;
8 8
avatarURL?: string;
9 9
active: boolean;
10 -
};
10 +
}
11 11
12 12
// Auth
13 13
export interface AuthResponse {
14 14
token: string;
15 15
user: User;
16 -
};
16 +
}
17 17
18 18
export interface VerifyTokenResponse {
19 19
valid: boolean;
20 20
userId?: number;
21 21
email?: string;
22 -
user: Pick<User, 'firstName' | 'lastName'>;
23 -
};
22 +
user: Pick<User, "firstName" | "lastName">;
23 +
}
24 24
25 25
export interface SignUpPayload {
26 26
firstName: string;
27 27
lastName?: string;
28 28
email: string;
29 29
password: string;
30 -
};
30 +
}
31 31
32 32
export interface ResetPasswordRequestResponse {
33 33
message: string;
34 34
token?: string;
35 -
};
35 +
}
36 36
37 37
export interface ResetPasswordResponse {
38 38
message: string;
39 -
};
39 +
}
40 40
41 41
export interface ActivateResponse {
42 42
activated: boolean;
43 43
}
44 44
46 46
export interface UserUpdatePayload extends Partial<User> {
47 47
id: number;
48 48
}
49 49
50 50
export interface UserUpdateResponse {
51 -
user: User
52 -
};
51 +
user: User;
52 +
}
53 53
54 54
// Mood entries
55 55
export interface MoodComponentPayload {
56 56
component: string;
57 57
intensity: string;
58 -
};
58 +
}
59 59
60 60
export interface CreateMoodEntryPayload {
61 61
annotation: string;
62 62
moment: Date;
63 63
selectedMood: string;
64 64
anxietyLevel: number;
65 65
energyLevel: number;
66 66
stressLevel: number;
67 -
moodComponents: MoodComponentPayload[]
68 -
};
67 +
moodComponents: MoodComponentPayload[];
68 +
}
69 69
70 70
export interface MoodComponent {
71 71
id: number;
72 72
component: string;
73 73
intensity: string;
92 92
93 93
export interface SleepRecordPayload {
94 94
average: number;
95 95
annotations: string;
96 96
date: Date;
97 -
};
97 +
}
98 98
99 99
export interface SleepRecord {
100 100
id: number;
101 101
annotations: string;
102 102
date: Date;
103 103
average: number;
104 104
createdAt: Date;
105 105
updatedAt: Date;
106 106
}
107 107
108 +
// Trigger
109 +
110 +
export interface Trigger {
111 +
id: number;
112 +
comment: string;
113 +
category: string;
114 +
moment: Date;
115 +
createdAt: Date;
116 +
updatedAt: Date;
117 +
}
118 +
119 +
export interface CreateTriggerPayload {
120 +
comment: string;
121 +
category: string;
122 +
moment: Date;
123 +
}
124 +
108 125
// Generic stuff
109 126
export interface PaginatedResponse<T> {
110 127
entries: T[];
111 128
total: number;
112 129
page: number;
113 130
nextPage: number | null;
114 -
};
131 +
}