frontend: display triggers on the history page
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
ee13593e49f8dd33700f17862fb841e7660653ce Browse filesParents
d3c5b58Changed files
7
Additions
+221
Deletions
-109
frontend/app/(tabs)/history.tsx
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
))}
47 58
</ScrollView>
48 59
</ScreenLayout>
49 60
);
50 61
}
frontend/app/triggers/new.tsx
1 1
import {
2 2
View,
3 3
Text,
4 4
StyleSheet,
5 5
KeyboardAvoidingView,
21 21
22 22
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",
30 30
value: "SOCIAL",
31 31
icon: <MaterialIcons size={16} color="#AF52DE" name="diversity-1" />,
165 165
},
166 166
keyboardView: {
167 167
flex: 1,
168 168
},
169 169
});
/dev/null → frontend/components/history/TriggerCard.tsx
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 -
import { useRef } from 'react';
1 +
import { useRef } from "react";
2 2
import {
3 3
ScrollView,
4 4
TouchableOpacity,
5 5
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) {
31 32
const scrollRef = useRef<ScrollView>(null);
32 33
37 38
horizontal
38 39
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}
46 47
onPress={() => onChange(filter.value)}
47 48
activeOpacity={0.7}
49 50
>
50 51
<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
58 59
{/*
59 60
TODO: needs a new build, probably
64 65
style={styles.fadeRight}
65 66
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,
93 94
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,
109 110
width: 48,
110 111
},
111 112
});
frontend/hooks/useHistoryFeed.ts
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 -
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 -
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 +
};