eletrotupi / tcc/ commit / 4ef759a

frontend: introduce triggers hooks + dispatch save to the API

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 4ef759a7c84b0f1067b873dcf561cc752569a817 Browse files
Parents cd79c89
Changed 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 +
}