frontend: display triggers on the history page

Pedro Lucas Porcellis porcellis@eletrotupi.com 14 days ago ee13593e49f8dd33700f17862fb841e7660653ce
Parents: d3c5b58
7 file(s) changed
  • frontend/app/(tabs)/history.tsx +35 -24
  • frontend/app/triggers/new.tsx +1 -1
  • frontend/components/history/TriggerCard.tsx +82 -0
  • frontend/components/ui/FilterPills.tsx +25 -24
  • frontend/hooks/useHistoryFeed.ts +22 -16
  • frontend/lib/history/mappers.ts +43 -31
  • frontend/lib/history/types.ts +13 -13
frontend/app/(tabs)/history.tsx
@@ -1,46 +1,57 @@
1 - import { useState } from 'react';
2 - import { ScrollView, View, Text } from 'react-native'
3 - import { ScreenLayout } from '@/components/ui/ScreenLayout';
4 - import { Section, SectionHeader } from '@/components/ui/Sections';
5 - import { FilterPills } from '@/components/ui/FilterPills';
6 - import { useAuth } from '@/context/AuthContext';
7 - import { useHistoryFeed } from '@/hooks/useHistoryFeed';
8 - import type { HistoryCategory } from '@/lib/history/types';
9 - import { MoodHistoryCard } from '@/components/history/MoodHistoryCard'
10 - import { SleepRecordCard } from '@/components/history/SleepRecordCard'
1 + import { useState } from "react";
2 + import { ScrollView, View, Text } from "react-native";
3 + import { ScreenLayout } from "@/components/ui/ScreenLayout";
4 + import { Section, SectionHeader } from "@/components/ui/Sections";
5 + import { FilterPills } from "@/components/ui/FilterPills";
6 + import { useAuth } from "@/context/AuthContext";
7 + import { useHistoryFeed } from "@/hooks/useHistoryFeed";
8 + import type { HistoryCategory } from "@/lib/history/types";
9 + import { MoodHistoryCard } from "@/components/history/MoodHistoryCard";
10 + import { SleepRecordCard } from "@/components/history/SleepRecordCard";
11 + import { TriggerHistoryCard } from "@/components/history/TriggerCard";
11 12
12 - const FILTERS: { label: string; value: HistoryCategory | 'all' }[] = [
13 - { label: 'Tudo', value: 'all' },
14 - { label: 'Humor', value: 'mood' },
15 - { label: 'Sono', value: 'sleep' }
13 + const FILTERS: { label: string; value: HistoryCategory | "all" }[] = [
14 + { label: "Tudo", value: "all" },
15 + { label: "Humor", value: "mood" },
16 + { label: "Sono", value: "sleep" },
17 + { label: "Gatilho", value: "trigger" },
16 18 ];
17 19
18 20 function HistoryCardRenderer({ card }: { card: HistoryCard }) {
19 21 switch (card.category) {
20 - case 'mood': return <MoodHistoryCard card={card as any} />
21 - case 'sleep': return <SleepRecordCard card={card as any} />
22 + case "mood":
23 + return <MoodHistoryCard card={card as any} />;
24 + case "sleep":
25 + return <SleepRecordCard card={card as any} />;
26 + case "trigger":
27 + return <TriggerHistoryCard card={card as any} />;
22 28 }
23 29 }
24 30
25 31 export default function History() {
26 32 const { user } = useAuth();
27 - const [activeFilter, setActiveFilter] = useState<HistoryCategory | 'all'>('all');
33 + const [activeFilter, setActiveFilter] = useState<HistoryCategory | "all">(
34 + "all",
35 + );
28 36 const grouped = useHistoryFeed(activeFilter);
29 37
30 38 return (
31 - <ScreenLayout userName={user.firstName} userAvatar={user.avatarURL}
32 - onNotificationPress={() => console.log('Notifications')}
33 - showNotificationBadge={true}>
39 + <ScreenLayout
40 + userName={user.firstName}
41 + userAvatar={user.avatarURL}
42 + onNotificationPress={() => console.log("Notifications")}
43 + showNotificationBadge={true}
44 + >
34 45 <ScrollView>
35 - <FilterPills active={activeFilter} onChange={setActiveFilter} />
46 + <FilterPills active={activeFilter} onChange={setActiveFilter} />
36 47
37 48 {Object.entries(grouped).map(([date, cards]) => (
38 49 <Section key={date}>
39 50 <SectionHeader title={date} />
40 51 <View>
41 - {cards.map(card => (
42 - <HistoryCardRenderer key={card.id} card={card} />)
43 - )}
52 + {cards.map((card) => (
53 + <HistoryCardRenderer key={card.id} card={card} />
54 + ))}
44 55 </View>
45 56 </Section>
46 57 ))}
frontend/app/triggers/new.tsx
@@ -23,7 +23,7 @@ const TRIGGER_CATEGORIES: CategoryOption<TriggerCategory>[] = [
23 23 {
24 24 label: "Trabalho",
25 25 value: "WORK",
26 - icon: <Ionicons size={16} color="#60A5FA" name="briefcase" />,
26 + icon: <MaterialIcons size={16} color="#60A5FA" name="work" />,
27 27 },
28 28 {
29 29 label: "Social",
frontend/components/history/TriggerCard.tsx
@@ -0,0 +1,82 @@
1 + import { Text, View, StyleSheet } from "react-native";
2 + import { MaterialIcons } from "@expo/vector-icons";
3 + import { Card } from "@/components/ui/Cards";
4 + import { Badge } from "@/components/ui/Badge";
5 + import { HistoryCard } from "@/lib/history/types";
6 + import { Trigger } from "@/lib/api";
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 + };
21 +
22 + type Props = { card: HistoryCard & { raw: Trigger } };
23 +
24 + export function TriggerHistoryCard({ card }: Props) {
25 + const meta = TRIGGER_META[card.raw.category] ?? TRIGGER_META.OTHER;
26 + const time = new Date(card.timestamp).toLocaleTimeString("pt-BR", {
27 + hour: "2-digit",
28 + minute: "2-digit",
29 + });
30 +
31 + return (
32 + <Card style={styles.card}>
33 + <View style={styles.row}>
34 + <View style={[styles.iconWrap, { backgroundColor: "#fff7ed" }]}>
35 + <MaterialIcons name={meta.icon} size={22} color="#f97316" />
36 + </View>
37 + <View style={styles.body}>
38 + <View style={styles.titleRow}>
39 + <Text style={styles.title}>Gatilho Identificado</Text>
40 + <Text style={styles.time}>{time}</Text>
41 + </View>
42 + {card.raw.comment ? (
43 + <Text style={styles.summary} numberOfLines={2}>
44 + {card.raw.comment}
45 + </Text>
46 + ) : null}
47 + <Badge
48 + label={meta.label}
49 + variant={meta.variant as any}
50 + style={styles.badge}
51 + />
52 + </View>
53 + </View>
54 + </Card>
55 + );
56 + }
57 +
58 + const styles = StyleSheet.create({
59 + card: {
60 + padding: Spacing.cardGap,
61 + },
62 + row: { flexDirection: "row", alignItems: "flex-start", gap: 12 },
63 + iconWrap: {
64 + width: 40,
65 + height: 40,
66 + borderRadius: 20,
67 + alignItems: "center",
68 + justifyContent: "center",
69 + marginTop: 1,
70 + },
71 + body: { flex: 1 },
72 + titleRow: {
73 + flexDirection: "row",
74 + justifyContent: "space-between",
75 + alignItems: "center",
76 + marginBottom: 2,
77 + },
78 + title: { fontSize: 15, fontWeight: "600" },
79 + time: { fontSize: 13, opacity: 0.45 },
80 + summary: { fontSize: 13, opacity: 0.6, lineHeight: 18, marginBottom: 6 },
81 + badge: { alignSelf: "flex-start", marginTop: 4 },
82 + });
frontend/components/ui/FilterPills.tsx
@@ -1,4 +1,4 @@
1 - import { useRef } from 'react';
1 + import { useRef } from "react";
2 2 import {
3 3 ScrollView,
4 4 TouchableOpacity,
@@ -6,25 +6,26 @@ Text,
6 6 View,
7 7 StyleSheet,
8 8 Platform,
9 - } from 'react-native';
10 - import { LinearGradient } from 'expo-linear-gradient';
11 - import type { HistoryCategory } from '@/lib/history/types';
12 - import { Colors } from '@/constants/theme';
9 + } from "react-native";
10 + import { LinearGradient } from "expo-linear-gradient";
11 + import type { HistoryCategory } from "@/lib/history/types";
12 + import { Colors } from "@/constants/theme";
13 13
14 14 export type FilterOption = {
15 - label: string
16 - value: HistoryCategory | 'all'
15 + label: string;
16 + value: HistoryCategory | "all";
17 17 };
18 18
19 19 export const HISTORY_FILTERS: FilterOption[] = [
20 - { label: 'Tudo', value: 'all' },
21 - { label: 'Humor', value: 'mood' },
22 - { label: 'Sono', value: 'sleep' }
20 + { label: "Tudo", value: "all" },
21 + { label: "Humor", value: "mood" },
22 + { label: "Sono", value: "sleep" },
23 + { label: "Gatilho", value: "trigger" },
23 24 ];
24 25
25 26 type Props = {
26 - active: HistoryCategory | 'all'
27 - onChange: (value: HistoryCategory | 'all') => void
27 + active: HistoryCategory | "all";
28 + onChange: (value: HistoryCategory | "all") => void;
28 29 };
29 30
30 31 export function FilterPills({ active, onChange }: Props) {
@@ -39,7 +40,7 @@ showsHorizontalScrollIndicator={false}
39 40 contentContainerStyle={styles.scrollContent}
40 41 >
41 42 {HISTORY_FILTERS.map((filter) => {
42 - const isActive = filter.value === active
43 + const isActive = filter.value === active;
43 44 return (
44 45 <TouchableOpacity
45 46 key={filter.value}
@@ -51,7 +52,7 @@ <Text style={[styles.label, isActive && styles.labelActive]}>
51 52 {filter.label}
52 53 </Text>
53 54 </TouchableOpacity>
54 - )
55 + );
55 56 })}
56 57 </ScrollView>
57 58
@@ -66,27 +67,27 @@ pointerEvents="none"
66 67 />
67 68 */}
68 69 </View>
69 - )
70 + );
70 71 }
71 72
72 73 const styles = StyleSheet.create({
73 74 wrapper: {
74 - position: 'relative',
75 + position: "relative",
75 76 },
76 77 scrollContent: {
77 78 paddingHorizontal: 16,
78 79 paddingVertical: 12,
79 80 gap: 8,
80 - flexDirection: 'row',
81 + flexDirection: "row",
81 82 },
82 83 pill: {
83 84 paddingHorizontal: 16,
84 85 paddingVertical: 7,
85 86 borderRadius: 999,
86 - backgroundColor: '#F2F2F7',
87 + backgroundColor: "#F2F2F7",
87 88 borderWidth: 1,
88 - borderColor: 'transparent',
89 - elevation: 1
89 + borderColor: "transparent",
90 + elevation: 1,
90 91 },
91 92 pillActive: {
92 93 backgroundColor: Colors.light.tint,
@@ -94,15 +95,15 @@ borderColor: Colors.light.tint,
94 95 },
95 96 label: {
96 97 fontSize: 14,
97 - fontWeight: '500',
98 - color: '#3C3C43',
98 + fontWeight: "500",
99 + color: "#3C3C43",
99 100 letterSpacing: -0.1,
100 101 },
101 102 labelActive: {
102 - color: Colors.light.text
103 + color: Colors.light.text,
103 104 },
104 105 fadeRight: {
105 - position: 'absolute',
106 + position: "absolute",
106 107 right: 0,
107 108 top: 0,
108 109 bottom: 0,
frontend/hooks/useHistoryFeed.ts
@@ -1,31 +1,37 @@
1 1 // hooks/useHistoryFeed.ts
2 - import { useMemo } from 'react'
3 - import { mergeAndSort, filterByCategory, groupByDate } from '@/lib/utils/history'
2 + import { useMemo } from "react";
3 + import {
4 + mergeAndSort,
5 + filterByCategory,
6 + groupByDate,
7 + } from "@/lib/utils/history";
4 8 import {
5 9 mapMoodToHistoryCard,
6 10 mapSleepToHistoryCard,
7 11 mapTriggerToHistoryCard,
8 - mapInterventionToHistoryCard
9 - } from '@/lib/history/mappers'
10 - import type { HistoryCategory } from '@/lib/history/types'
11 - import { useMoodEntries, useSleepRecords } from '@/hooks';
12 + } from "@/lib/history/mappers";
13 + import type { HistoryCategory } from "@/lib/history/types";
14 + import { useMoodEntries, useSleepRecords, useTriggers } from "@/hooks";
12 15
13 - export function useHistoryFeed(activeFilter: HistoryCategory | 'all') {
14 - const { data: moodsPage } = useMoodEntries({})
15 - const { data: sleepPage } = useSleepRecords({})
16 + export function useHistoryFeed(activeFilter: HistoryCategory | "all") {
17 + const { data: moodsPage } = useMoodEntries({});
18 + const { data: sleepPage } = useSleepRecords({});
19 + const { data: triggersPage } = useTriggers({});
16 20
17 - const moods = moodsPage?.entries ?? []
18 - const sleepRecords = sleepPage?.entries ?? []
21 + const moods = moodsPage?.entries ?? [];
22 + const sleepRecords = sleepPage?.entries ?? [];
23 + const triggers = triggersPage?.entries ?? [];
19 24
20 25 const grouped = useMemo(() => {
21 26 const merged = mergeAndSort([
22 27 moods.map(mapMoodToHistoryCard),
23 28 sleepRecords.map(mapSleepToHistoryCard),
24 - ])
25 - const filtered = filterByCategory(merged, activeFilter)
26 - return groupByDate(filtered)
27 - }, [moods, sleepRecords, activeFilter])
29 + triggers.map(mapTriggerToHistoryCard),
30 + ]);
31 + const filtered = filterByCategory(merged, activeFilter);
32 + return groupByDate(filtered);
33 + }, [moods, sleepRecords, triggers, activeFilter]);
28 34
29 35 // TODO: expose the isLoading here
30 - return grouped
36 + return grouped;
31 37 }
frontend/lib/history/mappers.ts
@@ -1,53 +1,65 @@
1 - import type { HistoryCard } from '@/lib/history/types'
2 - import type { MoodEntry, SleepRecord } from '@/lib/api';
3 - import { getMood } from '@/constants/moods';
4 - import { parse } from 'date-fns';
1 + import type { HistoryCard } from "@/lib/history/types";
2 + import type { MoodEntry, SleepRecord, Trigger } from "@/lib/api";
3 + import { getMood } from "@/constants/moods";
4 + import { parse } from "date-fns";
5 5
6 6 export function mapMoodToHistoryCard(mood: MoodEntry): HistoryCard {
7 - const moodLabel = getMood(mood.selectedMood) ?? mood.selectedMood
7 + const moodLabel = getMood(mood.selectedMood);
8 8 const components = mood.moodComponents
9 - .map(c => c.component.charAt(0) + c.component.slice(1).toLowerCase())
10 - .join(', ')
9 + .map((c) => c.component.charAt(0) + c.component.slice(1).toLowerCase())
10 + .join(", ");
11 11
12 - const summary = [
13 - components || null,
14 - mood.annotation || null,
15 - ].filter(Boolean).join(' · ')
12 + const summary = [components || null, mood.annotation || null]
13 + .filter(Boolean)
14 + .join(" · ");
16 15
17 16 return {
18 17 id: `mood-${mood.id}`,
19 - category: 'mood',
20 - timestamp: mood.moment,
21 - title: moodLabel,
22 - summary: summary || 'Sem detalhes',
18 + category: "mood",
19 + timestamp: mood.moment.toString(),
20 + title: moodLabel?.label ?? "Humor",
21 + summary: summary || "Sem detalhes",
23 22 badge: undefined,
24 - raw: mood
25 - }
23 + raw: mood,
24 + };
26 25 }
27 26
28 27 // Sleep Records
29 28
30 - function sleepQualityBadge(hours: number): HistoryCard['badge'] {
31 - if (hours >= 7) return { label: 'Excelente', variant: 'success' }
32 - if (hours >= 5) return { label: 'Regular', variant: 'warning' }
29 + function sleepQualityBadge(hours: number): HistoryCard["badge"] {
30 + if (hours >= 7) return { label: "Excelente", variant: "success" };
31 + if (hours >= 5) return { label: "Regular", variant: "warning" };
33 32
34 33 return {
35 - label: 'Ruim', variant: 'danger'
36 - }
34 + label: "Ruim",
35 + variant: "danger",
36 + };
37 37 }
38 38
39 39 export function mapSleepToHistoryCard(record: SleepRecord): HistoryCard {
40 - const hours = Math.floor(record.average)
41 - const minutes = Math.round((record.average - hours) * 60)
42 - const duration = `${hours}h ${minutes.toString().padStart(2, '0')}min`
40 + const hours = Math.floor(record.average);
41 + const minutes = Math.round((record.average - hours) * 60);
42 + const duration = `${hours}h ${minutes.toString().padStart(2, "0")}min`;
43 43
44 44 return {
45 45 id: `sleep-${record.id}`,
46 - category: 'sleep',
47 - timestamp: record.date,
48 - title: 'Sono',
49 - summary: [duration, record.annotations].filter(Boolean).join(' · '),
46 + category: "sleep",
47 + timestamp: record.date.toString(),
48 + title: "Sono",
49 + summary: [duration, record.annotations].filter(Boolean).join(" · "),
50 50 badge: sleepQualityBadge(record.average),
51 - raw: record
52 - }
51 + raw: record,
52 + };
53 + }
54 +
55 + export function mapTriggerToHistoryCard(record: Trigger): HistoryCard {
56 + return {
57 + id: `trigger-${record.id}`,
58 + category: "trigger",
59 + timestamp: record.moment.toString(),
60 + title: "Gatilho",
61 + summary: [record.category, record.comment].filter(Boolean).join(" · "),
62 + badge: undefined,
63 + raw: record,
64 + };
53 65 }
frontend/lib/history/types.ts
@@ -1,17 +1,17 @@
1 - import type { MoodEntry, SleepRecord } from '@/lib/api';
2 - export type HistoryCategory = 'mood' | 'sleep' | 'trigger' | 'intervention'
1 + import type { MoodEntry, SleepRecord, Trigger } from "@/lib/api";
2 + export type HistoryCategory = "mood" | "sleep" | "trigger" | "intervention";
3 3
4 4 export type HistoryBadge = {
5 - label: string
6 - variant: 'neutral' | 'warning' | 'success' | 'danger' | 'info'
7 - }
5 + label: string;
6 + variant: "neutral" | "warning" | "success" | "danger" | "info";
7 + };
8 8
9 9 export type HistoryCard = {
10 - id: string // "<category>-<id>" to avoid collisions
11 - category: HistoryCategory
12 - timestamp: string // ISO string — used for sorting and display
13 - title: string
14 - summary: string
15 - badge?: HistoryBadge,
16 - raw: MoodEntry | SleepRecord
17 - }
10 + id: string; // "<category>-<id>" to avoid collisions
11 + category: HistoryCategory;
12 + timestamp: string; // ISO string — used for sorting and display
13 + title: string;
14 + summary: string;
15 + badge?: HistoryBadge;
16 + raw: MoodEntry | SleepRecord | Trigger;
17 + };