frontend: introduce components for each history cards
Parents:
4cbb5182 file(s) changed
- frontend/components/history/MoodHistoryCard.tsx +114 -0
- frontend/components/history/SleepRecordCard.tsx +116 -0
frontend/components/history/MoodHistoryCard.tsx
@@ -0,0 +1,114 @@
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 + })
frontend/components/history/SleepRecordCard.tsx
@@ -0,0 +1,116 @@
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 + })