eletrotupi / tcc/ commit / f1583df

frontend: introduce a better insights tab + some basic components

Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago f1583dfe90160d2a63fcf9773f9ef6ba70337d4e
Parents: c456c24
4 file(s) changed
  • frontend/app/(tabs)/insights.tsx +139 -31
  • frontend/components/insights/InsightCard.tsx +84 -0
  • frontend/components/insights/WeeklyResume.tsx +87 -0
  • frontend/constants/theme.ts +11 -3
frontend/app/(tabs)/insights.tsx
1 -
import { ThemedText } from '@/components/misc/themed-text';
2 -
import { ThemedView } from '@/components/misc/themed-view';
3 -
import { ScreenLayout } from '@/components/ui/ScreenLayout';
4 -
import { useAuth } from '@/context/AuthContext';
5 -
import { Card, CardHeader } from '@/components/ui/Cards'
6 -
import { Section, SectionHeader } from '@/components/ui/Sections'
7 -
import { useThemeColor } from '@/hooks/use-theme-color'
8 -
import { Spacing, BorderRadius } from '@/constants/theme'
9 -
import { Grid } from '@/components/ui/LayoutHelpers'
1 +
import { View, Text, ScrollView, StyleSheet } from "react-native";
2 +

3 +
import { ThemedText } from "@/components/misc/themed-text";
4 +
import { ThemedView } from "@/components/misc/themed-view";
5 +
import { ScreenLayout } from "@/components/ui/ScreenLayout";
6 +
import { useAuth } from "@/context/AuthContext";
7 +
import { Card } from "@/components/ui/Cards";
8 +
import { Section, SectionHeader } from "@/components/ui/Sections";
9 +
import { useThemeColor } from "@/hooks/use-theme-color";
10 +
import { Spacing, BorderRadius, Colors, Typography } from "@/constants/theme";
11 +
import { WeeklyResume } from "@/components/insights/WeeklyResume";
12 +
import { InsightCard, Highlight } from "@/components/insights/InsightCard";
13 +
import { MoodTrendWidget } from "@/components/insights/MoodTrendWidget";
10 14

11 15
export default function Insights() {
12 16
  const { user } = useAuth();
13 -
  const backgroundColor = useThemeColor({}, 'background')
14 -
  const accentPurple = useThemeColor({}, 'accentPurple')
15 17

16 18
  return (
17 -
    <ScreenLayout userName={user.firstName}
19 +
    <ScreenLayout
20 +
      userName={user.firstName}
18 21
      userAvatar={user.avatarURL}
19 -
      onNotificationPress={() => console.log('Notifications')}
20 -
      showNotificationBadge={true}>
21 -
      <ThemedView>
22 -
        <Section>
23 -
          <SectionHeader
24 -
            title="Tendências da Semana"
25 -
            info="Últimos 7 dias"
26 -
          />
27 -
          <Card variant="default" style={{ height: 200 }} />
28 -
        </Section>
22 +
      onNotificationPress={() => console.log("Notifications")}
23 +
      showNotificationBadge={true}
24 +
    >
25 +
      <WeeklyResume
26 +
        avgMood="Neutro"
27 +
        stats={[
28 +
          { label: "Média de Sono", value: "7.2h" },
29 +
          { label: "Humor Estável", value: "92%" },
30 +
        ]}
31 +
      />
32 +

33 +
      <Section>
34 +
        <SectionHeader title="Indicadores" />
35 +

36 +
        <InsightCard
37 +
          category="Energia"
38 +
          body={
39 +
            <Text style={styles.insightBody}>
40 +
              Aumento de <Highlight>20%</Highlight> correlacionado ao sono.
41 +
            </Text>
42 +
          }
43 +
          metric="+20%"
44 +
          accent="green"
45 +
        />
29 46

30 -
        <Section>
31 -
          <SectionHeader title="Insights de Saúde" />
32 -
          <Grid gap={8}>
33 -
            <Card style={{ height: 150, backgroundColor: accentPurple }} />
34 -
            <Card style={{ height: 150, backgroundColor: 'powderblue' }} />
35 -
          </Grid>
36 -
        </Section>
37 -
      </ThemedView>
47 +
        <MoodTrendWidget />
48 +

49 +
        <InsightCard
50 +
          category="Gatilhos"
51 +
          body="Trabalho representa 45% dos registros."
52 +
          metric="45%"
53 +
          accent="neutral"
54 +
        />
55 +
      </Section>
38 56
    </ScreenLayout>
39 -
  )
57 +
  );
40 58
}
59 +

60 +
const styles = StyleSheet.create({
61 +
  scrollView: {
62 +
    flex: 1,
63 +
    backgroundColor: Colors.light.background,
64 +
  },
65 +
  contentContainer: {
66 +
    padding: Spacing.containerPadding,
67 +
    paddingBottom: 40,
68 +
  },
69 +

70 +
  // Hero card
71 +
  heroCard: {
72 +
    backgroundColor: Colors.light.textBlack,
73 +
    borderRadius: BorderRadius.lg,
74 +
    padding: 20,
75 +
    marginBottom: Spacing.sectionGap,
76 +
  },
77 +
  heroLabel: {
78 +
    fontSize: 11,
79 +
    fontWeight: "600",
80 +
    letterSpacing: 0.8,
81 +
    textTransform: "uppercase",
82 +
    color: Colors.light.tint,
83 +
    marginBottom: 8,
84 +
  },
85 +
  heroScoreRow: {
86 +
    flexDirection: "row",
87 +
    alignItems: "center",
88 +
    gap: 10,
89 +
  },
90 +
  heroScore: {
91 +
    ...Typography.headlineXg,
92 +
    fontSize: 52,
93 +
    color: "#ffffff",
94 +
  },
95 +
  heroTrendIcon: {
96 +
    marginTop: 6,
97 +
  },
98 +
  heroDivider: {
99 +
    height: StyleSheet.hairlineWidth,
100 +
    backgroundColor: "rgba(255,255,255,0.12)",
101 +
    marginVertical: 16,
102 +
  },
103 +
  heroStats: {
104 +
    flexDirection: "row",
105 +
    gap: Spacing.sectionGap,
106 +
  },
107 +
  heroStatItem: {
108 +
    flex: 1,
109 +
  },
110 +
  heroStatLabel: {
111 +
    fontSize: 12,
112 +
    color: "rgba(255,255,255,0.45)",
113 +
    fontWeight: "500",
114 +
    marginBottom: 4,
115 +
  },
116 +
  heroStatValue: {
117 +
    ...Typography.bodyLg,
118 +
    fontSize: 20,
119 +
    color: "#ffffff",
120 +
  },
121 +

122 +
  // Insight cards
123 +
  insightCardInner: {
124 +
    flexDirection: "row",
125 +
    alignItems: "center",
126 +
    justifyContent: "space-between",
127 +
    gap: 12,
128 +
  },
129 +
  insightCardLeft: {
130 +
    flex: 1,
131 +
    gap: 6,
132 +
  },
133 +
  insightCategory: {
134 +
    fontSize: 11,
135 +
    fontWeight: "600",
136 +
    letterSpacing: 0.8,
137 +
    textTransform: "uppercase",
138 +
    color: Colors.light.textSecondary,
139 +
  },
140 +
  insightBody: {
141 +
    ...Typography.bodyMd,
142 +
    color: Colors.light.text,
143 +
  },
144 +
  insightMetric: {
145 +
    ...Typography.headlineLg,
146 +
    fontSize: 22,
147 +
  },
148 +
});
frontend/components/insights/InsightCard.tsx
@@ -0,0 +1,84 @@
1 + import React from "react";
2 + import { View, Text, StyleSheet } from "react-native";
3 +
4 + import { Card } from "@/components/ui/Cards";
5 + import { Colors, Spacing, Typography } from "@/constants/theme";
6 +
7 + export type MetricAccent = "green" | "purple" | "neutral";
8 +
9 + const metricColor: Record<MetricAccent, string> = {
10 + green: Colors.light.tint,
11 + purple: Colors.light.accentDarkPurple,
12 + neutral: Colors.light.text,
13 + };
14 +
15 + interface InsightCardProps {
16 + category: string;
17 + body: React.ReactNode;
18 + metric: string;
19 + accent?: MetricAccent;
20 + }
21 +
22 + export const InsightCard: React.FC<InsightCardProps> = ({
23 + category,
24 + body,
25 + metric,
26 + accent = "neutral",
27 + }) => (
28 + <Card style={styles.cardContainer}>
29 + <View style={styles.inner}>
30 + <View style={styles.left}>
31 + <Text style={styles.category}>{category}</Text>
32 + {typeof body === "string" ? (
33 + <Text style={styles.body}>{body}</Text>
34 + ) : (
35 + body
36 + )}
37 + </View>
38 + <Text style={[styles.metric, { color: metricColor[accent] }]}>
39 + {metric}
40 + </Text>
41 + </View>
42 + </Card>
43 + );
44 +
45 + interface HighlightProps {
46 + children: React.ReactNode;
47 + color?: string;
48 + }
49 +
50 + export const Highlight: React.FC<HighlightProps> = ({
51 + children,
52 + color = Colors.light.tint,
53 + }) => <Text style={{ color, fontWeight: "700" }}>{children}</Text>;
54 +
55 + const styles = StyleSheet.create({
56 + cardContainer: {
57 + padding: Spacing.cardGap,
58 + },
59 + inner: {
60 + flexDirection: "row",
61 + alignItems: "center",
62 + justifyContent: "space-between",
63 + gap: 12,
64 + },
65 + left: {
66 + flex: 1,
67 + gap: 6,
68 + },
69 + category: {
70 + fontSize: 11,
71 + fontWeight: "600",
72 + letterSpacing: 0.8,
73 + textTransform: "uppercase",
74 + color: Colors.light.textSecondary,
75 + },
76 + body: {
77 + ...Typography.bodyMd,
78 + color: Colors.light.text,
79 + },
80 + metric: {
81 + ...Typography.headlineLg,
82 + fontSize: 22,
83 + },
84 + });
frontend/components/insights/WeeklyResume.tsx
@@ -0,0 +1,87 @@
1 + import React from "react";
2 + import { View, Text, StyleSheet } from "react-native";
3 + import { Colors, Typography, Spacing, BorderRadius } from "@/constants/theme";
4 +
5 + interface WeeklyResumeProps {
6 + avgMood: string;
7 + stats: { label: string; value: string }[];
8 + }
9 +
10 + export const WeeklyResume: React.FC<WeeklyResumeProps> = ({
11 + avgMood,
12 + stats,
13 + }) => {
14 + return (
15 + <View style={styles.heroCard}>
16 + <Text style={styles.heroLabel}>Resumo Semanal</Text>
17 +
18 + <View style={styles.heroScoreRow}>
19 + <Text style={styles.heroScore}>{avgMood}</Text>
20 + </View>
21 +
22 + <View style={styles.heroDivider} />
23 +
24 + <View style={styles.heroStats}>
25 + {stats.map((stat) => (
26 + <View key={stat.label} style={styles.heroStatItem}>
27 + <Text style={styles.heroStatLabel}>{stat.label}</Text>
28 + <Text style={styles.heroStatValue}>{stat.value}</Text>
29 + </View>
30 + ))}
31 + </View>
32 + </View>
33 + );
34 + };
35 +
36 + const styles = StyleSheet.create({
37 + heroCard: {
38 + backgroundColor: Colors.light.black,
39 + borderRadius: BorderRadius.lg,
40 + padding: 20,
41 + marginBottom: Spacing.sectionGap,
42 + },
43 + heroLabel: {
44 + fontSize: 11,
45 + fontWeight: "600",
46 + letterSpacing: 0.8,
47 + textTransform: "uppercase",
48 + color: Colors.light.tint,
49 + marginBottom: 8,
50 + },
51 + heroScoreRow: {
52 + flexDirection: "row",
53 + alignItems: "center",
54 + gap: 10,
55 + },
56 + heroScore: {
57 + ...Typography.headlineXg,
58 + fontSize: 52,
59 + color: "#ffffff",
60 + },
61 + heroTrendIcon: {
62 + marginTop: 6,
63 + },
64 + heroDivider: {
65 + height: StyleSheet.hairlineWidth,
66 + backgroundColor: "rgba(255,255,255,0.12)",
67 + marginVertical: 16,
68 + },
69 + heroStats: {
70 + flexDirection: "row",
71 + gap: Spacing.sectionGap,
72 + },
73 + heroStatItem: {
74 + flex: 1,
75 + },
76 + heroStatLabel: {
77 + fontSize: 12,
78 + color: "rgba(255,255,255,0.45)",
79 + fontWeight: "500",
80 + marginBottom: 4,
81 + },
82 + heroStatValue: {
83 + ...Typography.bodyLg,
84 + fontSize: 20,
85 + color: "#ffffff",
86 + },
87 + });
frontend/constants/theme.ts
1 1
/**
2 2
 * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 3
 * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 4
 */
5 5

▸ 24 unchanged lines
30 30
const PILL_INDIGO = "#e0e7ff";
31 31
const PILL_PURPLE = "#f3e8ff";
32 32

33 33
const tintColorLight = "#12ED5C";
34 34
const tintColorSecondary = "#618A70";
35 -
const black = "#0F172A";
36 -
const gray = "#475569";
37 -
const lightGray = "#64748B";
35 +
const GRAY = "#475569";
36 +
const LIGHT_GRAY = "#64748B";
37 +
const LIGHT_SLATE_GRAY = "#94A3B8";
38 38

39 39
export const Colors = {
40 40
  light: {
41 41
    // Primary Actions & Focus
42 42
    text: TEXT_PRIMARY,
▸ 29 unchanged lines
72 72

73 73
    // Semantic Accents (for categorization)
74 74
    accentBlue: PILL_BLUE,
75 75
    accentIndigo: PILL_INDIGO,
76 76
    accentPurple: PILL_PURPLE,
77 +
    accentDarkPurple: "#7F77DD",
78 +

79 +
    // Some grays
80 +
    gray: GRAY,
81 +
    lightGray: LIGHT_GRAY,
82 +
    lightSlateGray: LIGHT_SLATE_GRAY,
83 +
    black: PRIMARY_DARK,
77 84

78 85
    // Disabled / Secondary
79 86
    disabled: "#cbd5e1",
80 87
    divider: BORDER_SUBTLE,
81 88
    sliderTracking: "#E2E8F0",
82 89
    sliderLabels: "#94a3b8",
90 +
    warning: "orange",
83 91
    success: "#22c55e",
84 92
    danger: "#EF4444",
85 93
    textBlack: PRIMARY_DARK,
86 94
  },
87 95
};
▸ 137 unchanged lines
225 233
  typography: Typography,
226 234
  spacing: Spacing,
227 235
  shadows: Shadows,
228 236
  borderRadius: BorderRadius,
229 237
};