eletrotupi / tcc/ commit / 9964daf

frontend: introduce components for each history cards

Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago 9964dafa6ca2a967df3965c0aebbd9e474e93d3a Browse files
Parents 4cbb518
Changed 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 +
})