frontend: introduce components for each history cards
Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago
9964dafa6ca2a967df3965c0aebbd9e474e93d3a Browse filesParents
4cbb518Changed files
2
Additions
+230
Deletions
0
/dev/null → frontend/components/history/MoodHistoryCard.tsx
1 +
// components/history/cards/MoodHistoryCard.tsx
2 +
import { View, Text, StyleSheet } from 'react-native';
3 +
import { MaterialIcons } from '@expo/vector-icons';
4 +
import { Card } from '@/components/ui/Cards';
5 +
import { Badge } from '@/components/ui/Badge';
6 +
import type { HistoryCard } from '@/lib/history/types';
7 +
import type { MoodEntry } from '@/lib/api';
8 +
import { Spacing } from '@/constants/theme';
9 +
import { getMood } from '@/constants/moods';
10 +
import { format } from 'date-fns';
11 +
12 +
function inferBadge(anxiety: number, stress: number, energy: number) {
13 +
if (anxiety >= 7) return { label: 'Ansiedade alta', variant: 'red' } as const
14 +
if (stress >= 7) return { label: 'Estresse alto', variant: 'orange' } as const
15 +
if (energy <= 3) return { label: 'Energia baixa', variant: 'yellow' } as const
16 +
17 +
return undefined
18 +
}
19 +
20 +
// TODO: Perhaps we could move these into the constants instead and use it everywhere?
21 +
const MOOD_META: Record<string, { color: string; bg: string }> = {
22 +
GREAT: { color: '#16a34a', bg: '#dcfce7' },
23 +
GOOD: { color: '#2563eb', bg: '#dbeafe' },
24 +
NEUTRAL: { color: '#d97706', bg: '#fef3c7' },
25 +
SAD: { color: '#7c3aed', bg: '#ede9fe' },
26 +
ANGRY: { color: '#dc2626', bg: '#fee2e2' },
27 +
}
28 +
29 +
type Props = { card: HistoryCard & { raw: MoodEntry } }
30 +
31 +
export function MoodHistoryCard({ card }: Props) {
32 +
const meta = MOOD_META[card.raw.selectedMood] ?? MOOD_META.NEUTRAL
33 +
const badge = inferBadge(card.raw.anxietyLevel, card.raw.stressLevel, card.raw.energyLevel)
34 +
const time = format(card.timestamp, 'HH:mm')
35 +
const moodDef = getMood(card.raw.selectedMood.toLowerCase());
36 +
37 +
return (
38 +
<Card style={styles.card}>
39 +
<View style={styles.row}>
40 +
<View style={[styles.iconWrap, { backgroundColor: meta.bg }]}>
41 +
<Text style={styles.moodIconEmoji}>{moodDef?.icon ?? '😐'}</Text>
42 +
43 +
{false && (<MaterialIcons name={meta.icon} size={22} color={meta.color} />)}
44 +
</View>
45 +
<View style={styles.body}>
46 +
<View style={styles.titleRow}>
47 +
<Text style={styles.title}>{moodDef.label}</Text>
48 +
<Text style={styles.time}>{time}</Text>
49 +
</View>
50 +
{card.raw.annotation ? (
51 +
<Text style={styles.summary} numberOfLines={2}>
52 +
{card.raw.annotation?.trim()}
53 +
</Text>
54 +
) : null}
55 +
{badge ? (
56 +
<Badge label={badge.label}
57 +
variant={badge.variant}
58 +
style={styles.badge}
59 +
/> : null
60 +
)}
61 +
</View>
62 +
</View>
63 +
</Card>
64 +
)
65 +
}
66 +
67 +
const styles = StyleSheet.create({
68 +
card: {
69 +
padding: Spacing.cardGap
70 +
},
71 +
row: {
72 +
flexDirection: 'row',
73 +
alignItems: 'flex-start',
74 +
gap: 12
75 +
},
76 +
iconWrap: {
77 +
width: 40,
78 +
height: 40,
79 +
borderRadius: 20,
80 +
alignItems: 'center',
81 +
justifyContent: 'center',
82 +
marginTop: 1
83 +
},
84 +
body: { flex: 1 },
85 +
titleRow: {
86 +
flexDirection: 'row',
87 +
justifyContent: 'space-between',
88 +
alignItems: 'center',
89 +
marginBottom: 2
90 +
},
91 +
title: {
92 +
fontSize: 15,
93 +
fontWeight: '600'
94 +
},
95 +
moodIconEmoji: {
96 +
fontSize: 26,
97 +
},
98 +
time: {
99 +
fontSize: 13,
100 +
opacity: 0.45
101 +
},
102 +
summary: {
103 +
fontSize: 13,
104 +
opacity: 0.6,
105 +
lineHeight: 18,
106 +
marginBottom: 6,
107 +
fontStyle: 'italic'
108 +
},
109 +
badge: {
110 +
alignSelf: 'flex-start',
111 +
marginTop: 4,
112 +
maxWidth: '50%'
113 +
}
114 +
})
/dev/null → frontend/components/history/SleepRecordCard.tsx
1 +
import { View, Text, 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 type { SleepRecord } from '@/lib/api'
6 +
import { Spacing } from '@/constants/theme';
7 +
8 +
function sleepBadge(hours: number) {
9 +
if (hours >= 7) return { label: 'Excelente', variant: 'green' } as const
10 +
if (hours >= 5) return { label: 'Regular', variant: 'yellow' } as const
11 +
12 +
return {
13 +
label: 'Ruim', variant: 'red'
14 +
} as const
15 +
}
16 +
17 +
// TODO: Move this into the time utilities
18 +
function formatDuration(average: number) {
19 +
const h = Math.floor(average)
20 +
const m = Math.round((average - h) * 60)
21 +
return `${h}h ${m.toString().padStart(2, '0')}min`
22 +
}
23 +
24 +
type Props = { card: HistoryCard & { raw: SleepRecord } }
25 +
26 +
export function SleepRecordCard({ card }: Props) {
27 +
const badge = sleepBadge(card.raw.average)
28 +
const duration = formatDuration(card.raw.average)
29 +
30 +
const time = new Date(card.timestamp)
31 +
.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
32 +
33 +
return (
34 +
<Card style={styles.card}>
35 +
<View style={styles.row}>
36 +
<View style={[styles.iconWrap, { backgroundColor: '#ede9fe' }]}>
37 +
<MaterialIcons name="bedtime" size={22} color="#7c3aed" />
38 +
</View>
39 +
<View style={styles.body}>
40 +
<View style={styles.titleRow}>
41 +
<Text style={styles.title}>Sono</Text>
42 +
<Text style={styles.time}>{time}</Text>
43 +
</View>
44 +
<View style={styles.durationRow}>
45 +
<Text style={styles.duration}>{duration}</Text>
46 +
</View>
47 +
{card.raw.annotations ? (
48 +
<Text style={styles.summary} numberOfLines={2}>
49 +
{card.raw.annotations.trim()}
50 +
</Text>
51 +
) : null}
52 +
<Badge label={badge.label} variant={badge.variant} style={styles.badge} />
53 +
</View>
54 +
</View>
55 +
</Card>
56 +
)
57 +
}
58 +
59 +
const styles = StyleSheet.create({
60 +
card: {
61 +
padding: Spacing.cardGap
62 +
},
63 +
row: {
64 +
flexDirection: 'row',
65 +
alignItems: 'flex-start',
66 +
gap: 12
67 +
},
68 +
iconWrap: {
69 +
width: 40,
70 +
height: 40,
71 +
borderRadius: 20,
72 +
alignItems: 'center',
73 +
justifyContent: 'center',
74 +
marginTop: 1
75 +
},
76 +
body: {
77 +
flex: 1
78 +
},
79 +
titleRow: {
80 +
flexDirection: 'row',
81 +
justifyContent: 'space-between',
82 +
alignItems: 'center',
83 +
marginBottom: 4
84 +
},
85 +
title: {
86 +
fontSize: 15,
87 +
fontWeight: '600'
88 +
},
89 +
time: {
90 +
fontSize: 13,
91 +
opacity: 0.45
92 +
},
93 +
durationRow: {
94 +
flexDirection: 'row',
95 +
alignItems: 'center',
96 +
gap: 8,
97 +
marginBottom: 4
98 +
},
99 +
duration: {
100 +
fontSize: 13,
101 +
fontWeight: '500',
102 +
opacity: 0.75
103 +
},
104 +
summary: {
105 +
fontSize: 13,
106 +
opacity: 0.6,
107 +
lineHeight: 18,
108 +
marginTop: 4,
109 +
fontStyle: 'italic'
110 +
},
111 +
badge: {
112 +
alignSelf: 'flex-start',
113 +
marginTop: 4,
114 +
maxWidth: '50%'
115 +
},
116 +
})