frontend: introduce a better insights tab + some basic components
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
f1583dfe90160d2a63fcf9773f9ef6ba70337d4e Browse filesParents
c456c24Changed files
4
Additions
+321
Deletions
-34
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 +
});
/dev/null → frontend/components/insights/InsightCard.tsx
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 +
});
/dev/null → frontend/components/insights/WeeklyResume.tsx
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
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,
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
};
225 233
typography: Typography,
226 234
spacing: Spacing,
227 235
shadows: Shadows,
228 236
borderRadius: BorderRadius,
229 237
};