eletrotupi / tcc/ commit / ee13593

frontend: display triggers on the history page

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